팀에서 개발한 프로젝트 중 CEF를 사용하는 윈도우 기반 프로젝트가 하나 있다. 이 프로젝트는 프론트엔드로 다양한 정보를 다양한 방식으로 제공하는데, 그 중 V8 Extension을 이용하기도 한다. 개발 도중 이곳에서 제공한 정보 중 한글이 깨진다는 제보를 받았다.
애플리케이션에서는 CefRegisterExtension
함수를 호출해서 커스텀 데이터를 등록하는데, 여기까지는 CefString
타입으로 넘기는게 다라 뭐 할 것도 없다. 그렇다면 CEF 이후의 단계에서 메롱한게 아닌가 싶어 CEF 심볼파일과 각종 소스코드를 받아 스텝 바이 스텝으로 비교해봤다.
증상
위에서도 말했지만 V8 Extension에 등록하는 글자가 깨지는 증상이다. 가령, 아래와 같이 등록했다고 하면, 실제 개발자 도구에서는 아래와 같이 보인다.
virtual void OnWebKitInitialized() override {
CefRegisterExtension(
L"cef-test",
L"var test_en = \'hello\';"
L"var test_ko = \'안녕\';"
L"var test_latin = \'ë°¿\';",
nullptr);
}
(치사하게도) test_en
만 제대로 보이고 test_ko
나 test_latin
은 처음 입력했던 글자대로 나오지 않는다. (한글을 그렇다 치고, 라틴문자를 왜 넣었는지는 아래 내용을 읽어보면 알게된다.)
원인
거두절미하고 결론만 말하면, CefRegisterExtension
함수에서의 구현이 잘못됐다. CefRegisterExtension
함수는 아래와 같이 구현되어 있다.
// https://bitbucket.org/chromiumembedded/cef/src/master/libcef/renderer/v8_impl.cc
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
// 생략...
class V8TrackString : public CefTrackNode {
public:
explicit V8TrackString(const std::string& str) : string_(str) {}
const char* GetString() { return string_.c_str(); }
private:
std::string string_;
};
// 생략...
bool CefRegisterExtension(const CefString& extension_name,
const CefString& javascript_code,
CefRefPtr<CefV8Handler> handler) {
// Verify that this method was called on the correct thread.
CEF_REQUIRE_RT_RETURN(false);
auto* isolate_manager = CefV8IsolateManager::Get();
V8TrackString* name = new V8TrackString(extension_name);
isolate_manager->AddGlobalTrackObject(name);
V8TrackString* code = new V8TrackString(javascript_code);
isolate_manager->AddGlobalTrackObject(code);
if (handler.get()) {
// The reference will be released when the process exits.
V8TrackObject* object = new V8TrackObject(isolate_manager->isolate());
object->SetHandler(handler);
isolate_manager->AddGlobalTrackObject(object);
}
std::unique_ptr<v8::Extension> wrapper(new ExtensionWrapper(
name->GetString(), code->GetString(), handler.get()));
blink::WebScriptController::RegisterExtension(std::move(wrapper));
return true;
}
// 생략...
이 함수는 전달한 extension_name
과 javascript_code
를 wrapper
에 넣어 blink::WebScriptController::RegisterExtension
에 전달한다. 둘 다 CefString
타입인데, 이 값을 바로 wrapper
로 넣지 않고 V8TrackString
객체를 생성한 다음 (각각 name
과 code
포인터 참고) 그 객체로부터 나온 문자열을 넘기고 있다.
얼핏보면 문제가 없는 코드로 보이나, V8TrackString
클래스는 CefString
타입의 객체를 받는 생성자가 없다. (특히 std::string
타입의 객체를 받는 생성자밖에 없다.) 그럼에도 빌드에 성공한다는 것은 CefString
타입이 어찌어찌 해서 std::string
타입으로 형 변환이 된다는 말인데, CefString
클래스를 보자.
// https://bitbucket.org/chromiumembedded/cef/src/master/include/internal/cef_string_wrappers.h
// Copyright (c) 2010 Marshall A. Greenblatt. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the name Chromium Embedded
// Framework nor the names of its contributors may be used to endorse
// or promote products derived from this software without specific prior
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
struct CefStringTraitsUTF16 {
typedef char16_t char_type;
typedef cef_string_utf16_t struct_type;
// 생략...
// Conversion methods.
static inline bool from_ascii(const char* str, size_t len, struct_type* s) {
return cef_string_ascii_to_utf16(str, len, s) ? true : false;
}
static inline std::string to_string(const struct_type* s) {
cef_string_utf8_t cstr;
memset(&cstr, 0, sizeof(cstr));
cef_string_utf16_to_utf8(s->str, s->length, &cstr);
std::string str;
if (cstr.length > 0) {
str = std::string(cstr.str, cstr.length);
}
cef_string_utf8_clear(&cstr);
return str;
}
// 생략...
};
template <class traits>
class CefStringBase final {
// 생략...
///
/// Return this string's data as a std::string. Translation will occur if
/// necessary based on the underlying string type.
///
std::string ToString() const {
if (empty()) {
return std::string();
}
return traits::to_string(string_);
}
// 생략...
///
/// Assignment operator overloads.
///
CefStringBase& operator=(const CefStringBase& str) {
FromString(str.c_str(), str.length(), true);
return *this;
}
operator std::string() const { return ToString(); }
// 생략...
};
typedef CefStringBase<CefStringTraitsWide> CefStringWide;
typedef CefStringBase<CefStringTraitsUTF8> CefStringUTF8;
typedef CefStringBase<CefStringTraitsUTF16> CefStringUTF16;
길어보이지만 필요한 것만 가져왔다. CString
은 cef_string.h 정의 상 CefStringUTF16
의 별칭 정도 된다. 주석을 좀 읽어보면 플랫폼 별 대응은 아니고 기본값이 UTF-16인 듯 하다. 윈도우에서 취급하는 유니코드도 대체로 UTF-16 (LE) 이다보니 CEF를 빌드할 때 별다른 수정은 필요하지 않겠다. (물론 CEF는 직접 빌드하기 보다는 CEf Automated Builds를 이용하는 편이다.)
위에서 말한대로 V8TrackString
클래스는 std::string
타입을 받는 생성자밖에 없다. CefString
(CefStringUTF16
) 타입이 V8TrackString
생성자에 전달되면, 아래와 같은 프로세스로 동작할 것이다.
CefStringBase<CefStringTraitsUTF16>::operator std::string() const
함수가 호출된다.CefStringBase<CefStringTraitsUTF16>::ToString() const
함수가 호출된다.- (문자열이 비어있지 않다면; 대부분..)
CefStringTraitsUTF16::to_string(const struct_type* s)
함수가 호출된다.struct_type
타입은 cef_string_types.h 에 정의되어 있는데, 문자열 포인터와 길이 정도 갖는 컨테이너로 간단히 정의되어 있다.
cef_string_utf16_to_utf8
함수를 호출해서 UTF-16 인코딩 되어있는 문자열을 UTF-8로 변환한다.cef_string_utf16_to_utf8
함수는 string_types_impl.cc 에 정의되어 있다.
- UTF-8로 변환한 문자열을
std::string
객체에 담아 리턴한다. (호출된 역순으로 리턴이 이어진다.)
즉, CefString
객체가 갖고있는 UTF-16 인코딩 문자열은 UTF-8 인코딩으로 변환하여 V8TrackString
의 생성자로 전달되고, 이후 V8 Extension 등록 프로세스에 이 변환된 문자열을 사용한다는 것이다.
이렇게 V8TrackString
타입의 객체로부터 만들어진 (+UTF-8 인코딩된) 문자열 name
과 code
는 ExtensionWrapper
타입의 생성자로 전달하여 동적할당 하고 있고, 이 동적할당 된 메모리 주소는 wrapper
가 가리키고 있다. 이 때, wrapper
의 타입 (std::unique_ptr<v8::Extension>
) 에서 보이다시피 ExtensionWrapper
클래스는 v8::Extension
을 상속하고 있다.
wrapper
객체는 blink::WebScriptController::RegisterExtension
함수에 전달되고 있는데, 이 함수의 콜스택을 나열해보면 아래와 같다. (순서대로 호출된다.)
blink::WebScriptController::RegisterExtension(wrapper)
(web_script_controller.cc - Chromium Code Search)ScriptController::RegisterExtensionIfNeeded(wrapper)
(script_controller.cc - Chromium Code Search)v8::RegisterExtension(wrapper)
(api.cc - Chromium Code Search)RegisteredExtension::Register(wrapper)
(api.cc - Chromium Code Search)
4번에서 RegisteredExtension::Register
함수에 진입하면 이 함수에서 관리하는 연결 리스트에 전달된 wrapper
객체를 추가하는 것으로 함수 호출이 마무리 된다. (바로 추가하진 않고 RegisteredExtension
타입으로 다시 감싸서 추가하는데 그렇게 중요한 내용은 아니라 생략한다.)
이렇게 리스트로 추가된 V8 Extension은 추후 V8 엔진 내부에서 리스트 순서대로 추가될 것이다. (연결 리스트에 삽입하는 로직상 CefRegisterExtension
함수가 호출된 순서 반대로 추가될 것이다.) 이제 추가된 연결 리스트를 차례로 실제 컴파일하는 코드로 가보자. RegisteredExtension
클래스 내 멤버함수인 first_extension()
으로 검색하는 것으로부터 시작된다.
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/init/bootstrapper.cc
// Copyright 2014 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 생략...
bool Genesis::CompileExtension(Isolate* isolate, v8::Extension* extension) {
Factory* factory = isolate->factory();
HandleScope scope(isolate);
Handle<SharedFunctionInfo> function_info;
Handle<String> source =
isolate->factory()
->NewExternalStringFromOneByte(extension->source())
.ToHandleChecked();
DCHECK(source->IsOneByteRepresentation());
// If we can't find the function in the cache, we compile a new
// function and insert it into the cache.
base::Vector<const char> name = base::CStrVector(extension->name());
SourceCodeCache* cache = isolate->bootstrapper()->extensions_cache();
Handle<Context> context(isolate->context(), isolate);
DCHECK(IsNativeContext(*context));
if (!cache->Lookup(isolate, name, &function_info)) {
Handle<String> script_name =
factory->NewStringFromUtf8(name).ToHandleChecked();
ScriptCompiler::CompilationDetails compilation_details;
MaybeHandle<SharedFunctionInfo> maybe_function_info =
Compiler::GetSharedFunctionInfoForScriptWithExtension(
isolate, source, ScriptDetails(script_name), extension,
ScriptCompiler::kNoCompileOptions, EXTENSION_CODE,
&compilation_details);
if (!maybe_function_info.ToHandle(&function_info)) return false;
cache->Add(isolate, name, function_info);
}
// 생략...
bool Genesis::InstallAutoExtensions(Isolate* isolate,
ExtensionStates* extension_states) {
for (v8::RegisteredExtension* it = v8::RegisteredExtension::first_extension();
it != nullptr; it = it->next()) {
if (it->extension()->auto_enable() &&
!InstallExtension(isolate, it, extension_states)) {
return false;
}
}
return true;
}
// 생략...
// Installs a named extension. This methods is unoptimized and does
// not scale well if we want to support a large number of extensions.
bool Genesis::InstallExtension(Isolate* isolate, const char* name,
ExtensionStates* extension_states) {
for (v8::RegisteredExtension* it = v8::RegisteredExtension::first_extension();
it != nullptr; it = it->next()) {
if (strcmp(name, it->extension()->name()) == 0) {
return InstallExtension(isolate, it, extension_states);
}
}
return Utils::ApiCheck(false, "v8::Context::New()",
"Cannot find required extension");
}
bool Genesis::InstallExtension(Isolate* isolate,
v8::RegisteredExtension* current,
ExtensionStates* extension_states) {
HandleScope scope(isolate);
if (extension_states->get_state(current) == INSTALLED) return true;
// The current node has already been visited so there must be a
// cycle in the dependency graph; fail.
if (!Utils::ApiCheck(extension_states->get_state(current) != VISITED,
"v8::Context::New()", "Circular extension dependency")) {
return false;
}
DCHECK(extension_states->get_state(current) == UNVISITED);
extension_states->set_state(current, VISITED);
v8::Extension* extension = current->extension();
// Install the extension's dependencies
for (int i = 0; i < extension->dependency_count(); i++) {
if (!InstallExtension(isolate, extension->dependencies()[i],
extension_states)) {
return false;
}
}
if (!CompileExtension(isolate, extension)) {
// We print out the name of the extension that fail to install.
// When an error is thrown during bootstrapping we automatically print
// the line number at which this happened to the console in the isolate
// error throwing functionality.
base::OS::PrintError("Error installing extension '%s'.\n",
current->extension()->name());
return false;
}
// 생략...
RegisteredExtension::first_extension()
함수가 호출되는 구문은 위 소스코드 구간이다. 연결 리스트의 첫 요소부터 하나씩 꺼낸 다음 위 코드 블록에서 맨 마지막에 정의된 Genesis::InstallExtension
함수 호출로 이어진다. 이 함수만 보자.
마지막에 정의된 Genesis::InstallExtension
함수 내부에서 의존성이 있는 V8 Extension을 먼저 (재귀 호출로) 모두 설치하고 나서 비로소 Genesis::CompileExtension
으로 전달된 JS 코드를 컴파일한다.
Genesis::CompileExtension
함수는 전달된 v8::Extension
객체 안에 넣어두었던 name
과 code
를 꺼내는 구간이 있는데, 아래 소스 코드 이다.
// Genesis::CompileExtension 함수에서 긁어옴
Handle<String> source =
isolate->factory()
->NewExternalStringFromOneByte(extension->source())
.ToHandleChecked();
v8::Extension::source()
함수를 호출하여 아까 UTF-8로 변환했던 code
를 꺼내어 v8::internal::Factory::NewExternalStringFromOneByte
함수에 전달하는데 이름이 좀 이상하다. one byte? UTF-8 인코딩이면 utf8 같은 접미사가 올 것 같은데 one byte라... 그렇다면 v8::Extension::source()
함수가 어떤걸 리턴하길래 여기다 넣는지 한 번 보자.
// https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-extension.h
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 생략...
class V8_EXPORT Extension {
// 생략...
const String::ExternalOneByteStringResource* source() const {
return source_;
}
// 생략...
};
// 생략...
음? v8::Extension::source()
함수도 String::ExternalOneByteStringResource
(의 포인터)를 리턴하네? 이게 무슨 클래스지? 정의를 한번 보자.
// https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-primitive.h
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 생략...
/**
* An ExternalOneByteStringResource is a wrapper around an one-byte
* string buffer that resides outside V8's heap. Implement an
* ExternalOneByteStringResource to manage the life cycle of the
* underlying buffer. Note that the string data must be immutable
* and that the data must be Latin-1 and not UTF-8, which would require
* special treatment internally in the engine and do not allow efficient
* indexing. Use String::New or convert to 16 bit data for non-Latin1.
*/
class V8_EXPORT ExternalOneByteStringResource
: public ExternalStringResourceBase {
// 생략...
이제 모든 실마리가 풀린 것 같다. ExternalOneByteStringResource
클래스에서 취급하는 문자열은 Latin-1 이란다. 심지어 UTF-8이 아니라고까지 써 두었다. (그렇다면 CefRegisterExtension
함수 주석에도 이러한 주의사항이 같이 들어갔으면 좋겠지만 아쉽게도 사용법과 예시만 적혀있다.)
검산
V8 소스코드를 쭉 훑어보면 위와 같은 깨짐 현상은 영문자 또는 라틴 문자가 아니면 모두 발생하는 것으로 봐도 된다. 그런데 한 가지 재밌는 점은, 한글은 그렇다 치더라도 라틴 문자는 왜 깨지는걸까? 답은 간단한데, 라틴 문자를 그대로 넣지 않고 UTF-8로 인코딩 했기 때문이다. 문자가 깨지지 않고 잘 표현되려면 UTF-16에서 UTF-8로 변환된 값이 Latin-1 테이블의 값과 동일해야 한다. 그 외에는 모두 깨지는데, 라틴 문자도 UTF-16에서 UTF-8로 변환되면서 Latin-1 테이블에 있는 글자에 정확히 대응하지 않아 깨진다.
처음 이 문제에 접근할 때는 소스코드를 분석해서 원인을 찾아가지는 않았고 깨진 글자의 값이 어떤 값인지 찾아가는 것으로부터 시작했다. 다음 그림을 다시 보자.
여기서 "hello", "안녕", "ë°¿"을 하나씩 보자. (라틴 문자에는 잘 몰라서 아무거나 넣었다.)
문자열 | unicode code point | UTF-16 | UTF-8 |
---|---|---|---|
hello | U+0068 U+0065 U+006C U+006C U+006F | 0x68 0x65 0x6C 0x6C 0x6F | 0x68 0x65 0x6C 0x6C 0x6F |
안녕 | U+C548 U+B155 | 0xC5 0x48 0xB1 0x55 | 0xEC 0x95 0x88 0xEB 0x85 0x95 |
ë°¿ | U+00EB U+00B0 U+00BF | 0x00 0xEB 0x00 0xB0 0x00 0xBF | 0xC3 0xAB 0xC2 0xB0 0xC2 0xBF |
UTF-16은 BMP에 위치한 글자는 그대로 표현하기 때문에 unicode code point와 값이 같다. UTF-16으로 표현된 글자는 다시 UTF-8로 변환되어 V8 Extension에 등록됨을 위에서 확인했다.
그런데 UTF-8 글자를 Latin-1 글자로 취급해서 이 문제가 발생했으니, UTF-8의 각 바이트 값을 Latin-1 테이블에 어디에 대응되는지 확인해보면 된다.
코드 표는 위키피디아 를 참고했다.
문자열 | UTF-8 | Latin-1 대응 글자 (대응되는 글자가 없으면 \xHH 식으로 표현함) |
---|---|---|
hello | 0x68 0x65 0x6C 0x6C 0x6F | hello |
안녕 | 0xEC 0x95 0x88 0xEB 0x85 0x95 | ì \x95 \x88 ë \x85 \x95 |
ë°¿ | 0xC3 0xAB 0xC2 0xB0 0xC2 0xBF | Ã « Â ° Â ¿ |
UTF-8 코드 값에 대응하는 Latin-1 대응 글자를 보면 이미지에 나온 것과 일치함을 알 수 있다. 한 가지 재밌는 점은, "°"와 "¿" 는 각각 UTF-8 인코딩 한 0xC2 0xB0
와 0xC2 0xBF
중 0xC2
와 0xBF
가 Latin-1 글자 표에서도 각각 "°"와 "¿" 인 점이다. 하지만 둘 다 0xC2
때문에 "Â" 가 붙었다. (까비..)
해결
가장 좋은 해결책은 V8 Extension이 다양한 글자를 지원하는 것이지만, 구조상 불가능 한 것인지 그렇게까지는 필요가 없다고 판단한 것인지는 모르겠다. 무엇보다 우리가 당장 해결할 수도 없기도 하고.
당장 CEF를 사용하면서 해결할 수 있는 방법은 우리가 표현하려는 UTF-16 인코딩된 문자가 UTF-8로 인코딩 되어 최종적으로 Latin-1 글자로 대응할 때 까지 그 원본을 유지하는 것이다. 그러기 위한 유일한 방법은 알파벳이나 숫자, 일부 특수문자 등 ASCII 범위 내에서 표현하는 것이다. (위의 "hello" 인코딩 참고)
방법이야 많겠지만 Base64 인코딩 또는 퍼센트 인코딩으로 한 번 인코딩 하여 전달하는 방식이 가장 접근하기 쉬울 것이다. 이 중에서 본인 기준에 쓰기 편한 퍼센트 인코딩으로 해결하려 한다. (Base64 인코딩 방식도 인코딩에 차이만 있을 뿐 동일하게 해결 가능하다.)
퍼센트 인코딩은 UTF-8 코드 값을 %XX
식으로 표현하는 방식인데, 표현하려는 글자를 퍼센트 인코딩 한 것을 다시 decodeURI
또는 decodeURIComponent
함수로 감싸서 V8 Extension을 구성하면 된다.
virtual void OnWebKitInitialized() override {
CefRegisterExtension(
L"cef-test",
L"var test_en = \'hello\';"
L"var test_ko = decodeURIComponent(\'%EC%95%88%EB%85%95\');" // 안녕
L"var test_latin = decodeURIComponent(\'%C3%AB%C2%B0%C2%BF\');", // ë°¿
nullptr);
}
교훈
CefRegisterExtension
함수에 전달되는 V8 Extension code가 영문자 또는 숫자 등 극히 일부 글자만 지원했다면 CEF 레벨에서도 주석이나 문서에 잘 표현했어야 하지 싶다. 그렇지 않다면 일종의 실수가 있었던 것 같은데, 실수가 맞다면 이번 경우는 CefString
이 std::string
으로 형 변환을 위한 연산자 오버로딩을 지원하는 바람에 자연스럽게 코드가 작성되어 그런 것 같다. 배푼 친절이 비수가 되었다고 해야할까.
C++로 개발을 하다보면 가끔 연산자 오버로딩을 제공하는게 맞는지에 대한 고민을 하게된다. 특히, C++ 개발자는 UTF-16을 std::wstring
으로, UTF-8을 std::string
으로 취급하곤 하는데, 타입이 갖고있는 데이터가 상이할 경우 (지금은 UTF-8과 Latin-1이 둘 다 std::string
으로 취급되지만 내용상으론 다르다.) 더욱 이런 오류를 내기 쉽다. 그래서 연산자 오버로딩 보다는 명시적으로 함수 이름에 나타내어 그 함수를 호출하는 것으로 호출 의도를 나타낼 수 있다.
C/C++에서는 구문적으로는 타입 체킹이 강할지 몰라도, "어떤 메모리 덩어리" 로 취급할 수 있기 때문에 어쩌면 타입 안정성에 취약할 수도 있다. (메모리 덩어리 취급이 유연하게 다루는 장점이 있기도 하다. 양날의 검이랄까.) 그런데 이번 경우에는 어찌보면 더욱 취급하기 어려운 경우인게, 명백하게 같은 타입(std::string
)임에도 타입 안에 들어있는 데이터의 종류(UTF-8과 Latin-1)가 달라 발생하는 문제라 문제 원인을 찾기가 더 어려울 수 있다.
C++에서는 std::basic_string<...>
을 문자열의 컨테이너 정도로만 다루는 것도 한 몫하는 듯 싶다. 과거에는 표현하려는 글자가 제한적이어서 큰 문제가 아니었으나, 이제는 인코딩에 대한 고려를 충분히 한 상태에서 조심히 다루어야 하겠다. (타입 자체에서 인코딩을 구별할 수 있는 장치가 없다보니 개발자가 알아서 조심해야겠다.)
정리하면, 그 타입이 갖고있는 데이터가 혼용해서 써도 되는지 아니면 허울(자료형)만 같은 별개의 것인지 잘 구분해야 한다.