JavaScript/Basics

[JavaScript] 문자열

Sonny Cucumber 2022. 3. 18. 18:44

자바스크립트엔 글자 하나만 저장할 수 있는 별도의 자료형이 없습니다.

텍스트 형식의 데이터는 길이에 상관없이 문자열 형태로 저장됩니다.

자바스크립트에서 문자열은 페이지 인코딩 방식과 상관없이 항상 UTF-16 형식을 따릅니다.

 

따옴표

따옴표의 종류가 무엇이 있었는지 상기해봅시다.

문자열은 작은따옴표나 큰따옴표, 백틱으로 감쌀 수 있습니다.

let single = '작은따옴표';
let double = "큰따옴표";

let backticks = `백틱`;

작은따옴표와 큰따옴표는 기능상 차이가 없습니다. 그런데 백틱엔 특별한 기능이 있습니다.

표현식을 ${…}로 감싸고 이를 백틱으로 감싼 문자열 중간에 넣어주면

해당 표현식을 문자열 중간에 쉽게 삽입할 수 있죠. 이런 방식을 템플릿 리터럴(template literal)이라고 부릅니다.

function sum(a, b) {
  return a + b;
}

alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.

백틱을 사용하면 문자열을 여러 줄에 걸쳐 작성할 수도 있습니다.

let guestList = `손님:
 * John
 * Pete
 * Mary
`;

alert(guestList); // 손님 리스트를 여러 줄에 걸쳐 작성함

자연스럽게 여러 줄의 문자열이 만들어졌네요. 작은따옴표나 큰따옴표를 사용하면 위와 같은 방식으로

여러 줄짜리 문자열을 만들 수 없습니다. 아래 예시를 실행해봅시다. 에러가 발생합니다.

let guestList = "손님: // Error: Invalid or unexpected token
  * John";

작은따옴표나 큰따옴표로 문자열을 표현하는 방식은 자바스크립트가 만들어졌을 때부터 있었습니다.

이때는 문자열을 여러 줄에 걸쳐 작성할 생각조차 못 했던 시기였죠.

백틱은 그 이후에 등장한 문법이기 때문에 따옴표보다 다양한 기능을 제공합니다.

백틱은 '템플릿 함수(template function)'에서도 사용됩니다. func`string` 같이

첫 번째 백틱 바로 앞에 함수 이름(func)을 써주면, 이 함수는 백틱 안의 문자열 조각이나

표현식 평가 결과를 인수로 받아 자동으로 호출됩니다. 이런 기능을 '태그드 템플릿(tagged template)'이라 부르는데,

태그드 템플릿을 사용하면 사용자 지정 템플릿에 맞는 문자열을 쉽게 만들 수 있습니다.

태그드 템플릿과 템플릿 함수에 대한 자세한 내용은 MDN 문서에서 확인해보세요. 참고로 이 기능은 자주 사용되진 않습니다.

특수 기호

'줄 바꿈 문자(newline character)'라 불리는 특수기호 \n을 사용하면

작은따옴표나 큰따옴표로도 여러 줄 문자열을 만들 수 있습니다.

let guestList = "손님:\n * John\n * Pete\n * Mary";

alert(guestList); // 손님 리스트를 여러 줄에 걸쳐 작성함

따옴표를 이용해 만든 여러 줄 문자열과 백틱을 이용해 만든 여러 줄 문자열은 표현 방식만 다를 뿐 차이가 없습니다.

 

let str1 = "Hello\nWorld"; // '줄 바꿈 기호'를 사용해 두 줄짜리 문자열을 만듦

// 백틱과 일반적인 줄 바꿈 방법(엔터)을 사용해 두 줄짜리 문자열을 만듦
let str2 = `Hello
World`;

alert(str1 == str2); // true

자바스크립트엔 줄 바꿈 문자를 비롯한 다양한 '특수'문자들이 있습니다.

특수 문자 목록:

\n 줄 바꿈
\r 캐리지 리턴(carriage return). Windows에선 캐리지 리턴과 줄 바꿈 특수 문자를 조합(\r\n)해 줄을 바꿉니다. 캐리지 리턴을 단독으론 사용하는 경우는 없습니다.
\', \" 따옴표
\\ 역슬래시
\t
\b, \f, \v 각각 백스페이스(Backspace), 폼 피드(Form Feed), 세로 탭(Vertical Tab)을 나타냅니다. 호환성 유지를 위해 남아있는 기호로 요즘엔 사용하지 않습니다.
\xXX 16진수 유니코드 XX로 표현한 유니코드 글자입니다(예시: 알파벳 'z'는 '\x7A'와 동일함).
\uXXXX UTF-16 인코딩 규칙을 사용하는 16진수 코드 XXXX로 표현한 유니코드 기호입니다. XXXX는 반드시 네 개의 16진수로 구성되어야 합니다(예시: \u00A9는 저작권 기호 ©의 유니코드임).
\u{X…XXXXXX}(한 개에서 여섯 개 사이의 16진수 글자) UTF-32로 표현한 유니코드 기호입니다. 몇몇 특수한 글자는 두 개의 유니코드 기호를 사용해 인코딩되므로 4바이트를 차지합니다. 이 방법을 사용하면 긴 코드를 삽입할 수 있습니다.

유니코드를 사용한 예시:

alert( "\u00A9" ); // ©
alert( "\u{20331}" ); // 佫, 중국어(긴 유니코드)
alert( "\u{1F60D}" ); // 😍, 웃는 얼굴 기호(긴 유니코드)

모든 특수 문자는 '이스케이프 문자(escape character)'라고도 불리는 역슬래시 (backslash character) \로 시작합니다.

역슬래시는 문자열 내에 따옴표를 넣을 때도 사용할 수 있습니다.

예시:

alert( 'I\'m the Walrus!' ); // I'm the Walrus!

위 예시에서 살펴본 바와 같이 문자열 내의 따옴표엔 \를 꼭 붙여줘야 합니다.

이렇게 하지 않으면 자바스크립트는 해당 따옴표가 문자열을 닫는 용도로 사용된 것이라 해석하기 때문입니다.

이스케이프 문자는 문자열을 감쌀 때 사용한 따옴표와 동일한 따옴표에만 붙여주면 됩니다.

문자열 내에서 좀 더 우아하게 따옴표를 사용하려면 아래와 같이 따옴표 대신 백틱으로 문자열을 감싸주면 됩니다.

alert( `I'm the Walrus!` ); // I'm the Walrus!

역슬래시 \는 문자열을 정확하게 읽기 위한 용도로 만들어졌으므로 \는 제 역할이 끝나면 사라집니다.

메모리에 저장되는 문자열엔 \가 없습니다. 앞선 예시들을 실행했을 때 뜨는 alert 창을 통해 이를 확인할 수 있습니다.

그렇다면 문자열 안에 역슬래시 \를 보여줘야 하는 경우엔 어떻게 해야 할까요?

\\같이 역슬래시를 두 개 붙이면 됩니다.

alert( `역슬래시: \\` ); // 역슬래시: \

문자열의 길이

length 프로퍼티엔 문자열의 길이가 저장됩니다.

alert( `My\n`.length ); // 3

참고- length는 프로퍼티 입니다.

자바스크립트 이외의 언어를 사용했던 개발자들은 str.length가 아닌
str.length()로 문자열의 길이를 알아내려고 하는 경우가 있습니다. 하지만 원하는 대로 동작하지 않습니다.
length는 함수가 아니고, 숫자가 저장되는 프로퍼티라는 점에 주의하시기 바랍니다. 뒤에 괄호를 붙일 필요가 없습니다.


특정 글자에 접근하기

문자열 내 특정 위치인 pos에 있는 글자에 접근하려면 [pos]같이 대괄호를 이용하거나 

str.charAt(pos)라는 메서드를 호출하면 됩니다. 위치는 0부터 시작합니다.

let str = `Hello`;

// 첫 번째 글자
alert( str[0] ); // H
alert( str.charAt(0) ); // H

// 마지막 글자
alert( str[str.length - 1] ); // o

근래에는 대괄호를 이용하는 방식을 사용합니다. charAt은 하위 호환성을 위해 남아있는 메서드라고 생각하시면 됩니다.

두 접근 방식의 차이는 반환할 글자가 없을 때 드러납니다. 접근하려는 위치에 글자가 없는 경우

 []는 undefined를, charAt은 빈 문자열을 반환합니다.

let str = `Hello`;

alert( str[1000] ); // undefined
alert( str.charAt(1000) ); // '' (빈 문자열)

for..of를 사용하면 문자열을 구성하는 글자를 대상으로 반복 작업을 할 수 있습니다.

for (let char of "Hello") {
  alert(char); // H,e,l,l,o (char는 순차적으로 H, e, l, l, o가 됩니다.)
}

문자열의 불변성

문자열은 수정할 수 없습니다. 따라서 문자열의 중간 글자 하나를 바꾸려고 하면 에러가 발생합니다.

직접 실습해봅시다.

let str = 'Hi';

str[0] = 'h'; // Error: Cannot assign to read only property '0' of string 'Hi'
alert( str[0] ); // 동작하지 않습니다.

이런 문제를 피하려면 완전히 새로운 문자열을 하나 만든 다음, 이 문자열을 str에 할당하면 됩니다.

예시:

let str = 'Hi';

str = 'h' + str[1]; // 문자열 전체를 교체함

alert( str ); // hi

유사한 예시는 이어지는 절에서 살펴보겠습니다.

대-소문자 변경하기

메서드 toLowerCase()와 toUpperCase()는 대문자를 소문자로, 소문자를 대문자로 변경(케이스 변경)시켜줍니다.

alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface

글자 하나의 케이스만 변경하는 것도 가능합니다.

alert( 'Interface'[0].toLowerCase() ); // 'i'

부분문자열 찾기

문자열에서 부분 문자열(substring)을 찾는 방법은 여러가지가 있습니다.

 

str.indexOf

첫번째 방법은 str.indexOf(substr, pos)메서드를 이용하는 것입니다.

 

이 메서드는 문자열 str의 pos에서부터 시작해, 부분 문자열 substr이 어디에 위치하는지를 찾아줍니다.

원하는 부분 문자열을 찾으면 위치를 반환하고 그렇지 않으면 -1을 반환합니다.

예시:

let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0, str은 'Widget'으로 시작함
alert( str.indexOf('widget') ); // -1, indexOf는 대·소문자를 따지므로 원하는 문자열을 찾지 못함

alert( str.indexOf("id") ); // 1, "id"는 첫 번째 위치에서 발견됨 (Widget에서 id)

str.indexOf(substr, pos)의 두번째 매개변수 pos는 선택적으로 사용할 수 있는데, 이를 명시하면

검색이 해당 위치부터 시작됩니다.

부분 문자열 "id"는 위치 1에서 처음 등장하는데, 두번째 인수에 2를 넘겨  "id"가 두번째로 등장하는 위치가 어디인지 알아봅시다.

let str = 'Widget with id';

alert( str.indexOf('id', 2) ) // 12

문자열 내 부분 문자열 전체를 대상으로 무언가를 하고 싶다면 반복문 안에 indexOf를 사용하면 됩니다.

반복문이 하나씩 돌 때마다 검색 시작 위치가 갱신되면서 indexOf가 새롭게 호출됩니다.

let str = 'As sly as a fox, as strong as an ox';

let target = 'as'; // as를 찾아봅시다.

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `위치: ${foundPos}` );
  pos = foundPos + 1; // 다음 위치를 기준으로 검색을 이어갑니다.
}

동일한 알고리즘을 사용해 코드만 짧게 줄이면 다음과 같습니다.

let str = "As sly as a fox, as strong as an ox";
let target = "as";

let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
  alert( `위치: ${pos}` );
}

 

참고 - str.lastIndexOf(substr, position)

str.lastIndexOf(substr, position)는 indexOf와 유사한 기능을 하는 메서드입니다.

문자열 끝에서부터 부분 문자열을 찾는다는 점만 다릅니다.

 

반환되는 부분 문자열 위치는 문자열 끝이 기준입니다.


if문의 조건식에 indexOf를 쓸 때 주의할 점이 하나 있습니다.

아래와 같이 코드를 작성하면 원하는 결과를 얻을 수 없습니다.

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("찾았다!"); // 의도한 대로 동작하지 않습니다.
}

str.indexOf("Widget")은 0을 반환하는데, if문에선 0을 false로 간주하므로 alert창이 뜨지 않음.

따라서 부분 문자열 여부를 검사하려면 아래와 같이 -1과 비교해야합니다.

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("찾았다!"); // 의도한 대로 동작합니다.
}

비트 NOT 연산자를 사용한 기법

오래전부터 전해오는 비트(bitwise) NOT 연산자 ~를 사용한 기법 하나를 소개해드리겠습니다.

비트 NOT 연산자는 피연산자를 32비트 정수로 바꾼 후(소수부는 모두 버려짐) 모든 비트를 반전합니다.

 

따라서 n이 비트 정수일 때 ~n 은 -(n+1)이 됩니다.

예시:

alert( ~2 ); // -3, -(2+1)과 같음
alert( ~1 ); // -2, -(1+1)과 같음
alert( ~0 ); // -1, -(0+1)과 같음
alert( ~-1 ); // 0, -(-1+1)과 같음

위 예시에서 본 바와 같이 부호가 있는 32비트 정수  n중,

~n을 0으로 만드는 경우는 n == -1일 때가 유일합니다.

 

이를 응용해서 indexOf가 -1을 반환하지 않는 경우를 if ( ~str.indexOf("...") )로 검사해 봅시다.

이렇게 ~str.indexOf("...")를 사용하면 코드의 길이를 줄일 수 있습니다.

let str = "Widget";

if (~str.indexOf("Widget")) {
  alert( '찾았다!' ); // 의도한 대로 동작합니다.
}

사실 이렇게 언어 특유의 기능을 사용해 직관적이지 않은 코드를 작성하는 것은 추천하지 않음.

그렇지만 위와 같은 기법은 오래된 스크립트에서 쉽게 만날 수 있기 때문에 알아두어야 합니다.

 

if (~str.indexOf(...)) 패턴의 코드를 만나면 '부분 문자열인지 확인'하는 코드라고 기억해둡시다.

 

참고로 -1 이외에도 ~연산자 적용 시 0을 반환하는 숫자는 다양합니다.

아주 큰 숫자에 ~ 연산자를 적용하면 32비트 정수로 바꾸는 고자어에서 잘림 현상이 발생하기 때문.

이런 숫자 중 가장 큰 숫자는 4294967295입니다(~4294967295는 0임). 

문자열이 아주 길지 않은 경우에만 ~연산자가 의도한대로 작동한다는 점을 알고 계시기 바랍니다.

 

모던 자바스크립트에선. includes 메서드(아래에서 배움)를 사여요해 부분 문자열 포함 여부를 검사합니다.

이런 기법은 오래된 자바 스크립트에서만 볼 수 있습니다.

includes, startsWith, endsWith

비교적 근래에 나온 메서드인 str.includes(substr, pos)는 str에 부분 문자열 substr이 있는지에 따라

true나 false를 반환합니다.

부분 문자열의 위치 정보는 필요하지 않고 포함 여부만 알고싶을 때 적합한 메서드입니다.

alert( "Widget with id".includes("Widget") ); // true

alert( "Hello".includes("Bye") ); // false

str.includes에도 str.indexOf처럼 두번째 인수를 넘기면 해당 위치부터 부분 문자열을 검색합니다.

alert( "Widget".includes("id") ); // true
alert( "Widget".includes("id", 3) ); // false, 세 번째 위치 이후엔 "id"가 없습니다.

메서드 str.startsWith와 str.endsWith는 메서드 이름 그대로 문자열 str이 특정 문자열로 시작하는지

(startWith)여부와 특정 문자열로 끝나는지(end with)여부를 확인할 때 사용할 수 있습니다.

alert( "Widget".startsWith("Wid") ); // true, "Widget"은 "Wid"로 시작합니다.
alert( "Widget".endsWith("get") ); // true, "Widget"은 "get"으로 끝납니다.

부분 문자열 추출하기

JS엔 부분 문자열 추출과 관련된 메서드가 세가지가 있습니다.

세가지 메서드 subString, substr, slice를 하나씩 알아보자

str.slice(strat [, end])

문자열의 start 부터 end까지 (end는 미포함)를 반환합니다.

예시:

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', 0번째부터 5번째 위치까지(5번째 위치의 글자는 포함하지 않음)
alert( str.slice(0, 1) ); // 's', 0번째부터 1번째 위치까지(1번째 위치의 자는 포함하지 않음)

두번째 인수가 생략된 경우엔, 명시한 위치부터 문자열 끝까지를 반환합니다.

let str = "stringify";
alert( str.slice(2) ); // ringify, 2번째부터 끝까지

start와 end 는 음수가 될 수 있습니다. 음수를 넘기면 문자열 끝에서부터 카운팅을 시작합니다.

let str = "stringify";

// 끝에서 4번째부터 시작해 끝에서 1번째 위치까지
alert( str.slice(-4, -1) ); // gif

str.subString(start [, end])

start 와 end 사이에 있는 문자열을 반환합니다.

substring은 slice와 아주 유사하지만 start가 end보다 커도 괜찮다는 데 차이가 있습니다.

예시:

let str = "stringify";

// 동일한 부분 문자열을 반환합니다.
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// slice를 사용하면 결과가 다릅니다.
alert( str.slice(2, 6) ); // "ring" (같음)
alert( str.slice(6, 2) ); // "" (빈 문자열)

substring은 음수 인수를 허용하지 않습니다. 음수는 0으로 처리됩니다.

str.substr(start [, length])

start에서부터 시작해 length개의 글자를 반환합니다.

substr 은 끝 위치 대신에 길이를 기준으로 문자열을 추출한다는 점에서 substring과 slice와 차이가 있습니다.

let str = "stringify";
alert( str.substr(2, 4) ); // ring, 두 번째부터 글자 네 개

첫번째 인수가 음수면 되에서부터 개수를 셉니다.

let str = "stringify";
alert( str.substr(-4, 2) ); // gi, 끝에서 네 번째 위치부터 글자 두 개

부분 문자열 추출과 관련된 메서드를 요약해 봅시다.

메서드                                                      추출할 부분 문자열                                                            음수 허용 여부(인수)

slice(start, end) start부터 end까지(end는 미포함) 음수 허용
substring(start, end) start와 end 사이 음수는 0으로 취급함
substr(start, length) start부터 length개의 글자 음수 허용
 

참고 - 어떤 메서드를 선택해야 하나요?

모두 사용해도 되지만 substr에는 단점이 하나 있다. substr는 코어 JS 명세서가 아닌, 구식 스크립트에 대응하기 위해

남겨둔 브라우저 전용 기능들을 명시해 놓은 부록 B(Annex B)에 정의되어 있습니다.

거의 모든 곳에서 이 메서드가 동작하긴 하지만 브라우저 이외의 호스트 환경에서는 제대로 동작하지 않을 수 있습니다.

 

남은 두 메서드 중 slice는 음수 인수를 허용한다는 측면에서 subString보다 좀 더 유연합니다. 메서드 이름도 더 짧구요.

따라서 세 메서드 중 slice만 외워놓고 사용해도 충분할 것 같습니다.


문자열 비교하기

비교 연산자 챕터에서 알아보았듯이 문자열을 비교할 땐 순서를 기준으로 글자끼리 비교가 이뤄집니다.

  1. 소문자는 대문자보다 항상 큽니다.
alert( 'a' > 'Z' ); // true

2. 발음 구별 기호 (diacritical mark)가 붙은 문자는 알파벳 순서 기준을 따르지 않습니다.

alert( 'Österreich' > 'Zealand' ); // true (Österreich는 오스트리아를 독일어로 표기한 것임 - 옮긴이)

이런 예외사항 때문에 이름순으로 국가를 나열할 때 예상치 못한 결과가 나올 수 있습니다. 사람들은 

Österreich가 Zealand보다 앞서 나올 것이라 예상하는데 그렇지 않죠.

 

JS 내부에서 문자열이 어떻게 표시되는지 상기하며 원인을 알아봅시다.모든 문자열은 UTF-16 을 사용해 인코딩 되는데, UTF-16에선 모든 글자가 숫자 형식의 코드와 매칭됩니다.코드로 글자를 얻거나 글자에서 연관 코드를 알아낼 수 있는 메서드는 다음과 같습니다.

str.codePointAt(pos)

pos에 위치한 글자의 코드를 반환합니다.

// 글자는 같지만 케이스는 다르므로 반환되는 코드가 다릅니다.
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90

 

String.fromCodePoint(code)

숫자 형식의 code에 대응하는 글자를 만들어줍니다.

alert( String.fromCodePoint(90) ); // Z

\u 뒤에 특정 글자에 대응하는 16진수 코드를 붙이는 방식으로도 원하는 글자를 만들 수 있습니다.

// 90을 16진수로 변환하면 5a입니다.
alert( '\u005a' ); // Z

이제 이 배경지식을 가지고 코드 65와 220 사이(라틴계열 알파벳과 기타 글자들이 여기에 포함됨)에

대응하는 글자들을 출력해봅시다.

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„
// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ

보이시나요? 대문자 알파벳이 가장 먼저 나오고 특수 문자 몇개가 나온 다음에 소문자 알파벳이 나온다.

Ö은 거의 마지막에 출력된다.

 

이제 왜 a < Z인지 아시겠죠?

글자는 글자에 대응하는 숫자 형식의 코드를 기준으로 비교됩니다. 코드가 크면 대응하는 글자 역시 

크다고 취급되죠. 따라서 a(코드 : 97)는 Z(코드: 90)보다 크다는 결론이 도출됩니다.

  • 알파벳 소문자의 코드는 대문자의 코드보다 크므로 소문자는 대문자 뒤에 옵니다.
  • Ö같은 글자는 일반 알파벳과 멀리 떨어져있고 코드는 알파벳 소문자의 코드보다 훨씬 큽니다.

문자열 제대로 비교하기

언어마다 문자 체계가 다르기 때문에 문자열을 '제대로' 비교하는 알고리즘을 만드는 건 생각보다 간단하지 않습니다.

문자열을 비교하려면 일단 페이지에서 어떤 언어를 사용하고 있는지 브라우저가 알아야 합니다.

다행히도 모던 브라우저 대부분이 국제화 관련 표준인 ECMA-402를 지원합니다. (IE10은 아쉽게도 Intl.js 라이브러리 써야함)

ECMA-402엔 언어가 다를 때 적용할 수 있는 문자열 비교 규칙과 이를 준수하는 메서드가 정의되어있습니다.

 

str.localeCompare(str2)를 호출하면 ECMA-402에서 정의한 규칙에 따라 str이 str2보다 작은지, 같은지,

큰지를 나타내주는 정수가 반환됩니다.

  • str이 str2보다 작으면 음수를 반환합니다.
  • str이 str2보다 크면 양수를 반환합니다.
  • str이 str2과 같으면 0을 반환합니다.

예시:

alert( 'Österreich'.localeCompare('Zealand') ); // -1

 

localeCompare엔 선택 인수 두 개를 더 전달할 수 있습니다. 기준이 되는 언어를 지정(아무것도 지정하지 않았다면

호스트 환경의 언어가 기준 언언가 됨)해주는 인수와 대-소문자를 구분할지나 "a"와 ""를 다르게 취급할지에 대한

것을 설정해주는 인수가 더 있죠.

문자열 심화

참고-심화학습

이번 절에선 문자열을 더 깊게 다룹니다. 이모티콘이나 일부 수학 기호, 상형 문자를 비롯한 희귀 기호 등을

다뤄야 한다면 앞으로 배울 내용이 유용하게 사용될 것입니다.

이런 글자들을 사용할 계획이 없다면 본 절을 넘어가도 됨.


서로게이트 쌍

자주 사용되는 글자들은 모두 2바이트 코드를 가지고 있습니다.

유럽권 언어에서 사용되는 글자, 숫자, 상형 문자 대다수는 2바이트 표현 체계를 사용합니다.

 

그런데 2바이트는 65,536(2의 16승)개의 조합밖에 만들어내지 못하기 때문에 현존하는 기호를 모두

표현하기에 충분하지 않습니다. 이를 극복하기 위해 사용 빈도가 낮은 기호는  '서로게이트 쌍(surrogate pair)'라

불리는 2바이트 글자들의 쌍을 사용해 인코딩합니다.

 

서로게이트 쌍을 사용해 인코딩한 기호의 길이는 2입니다.

alert( '𝒳'.length ); // 2, 수학에서 쓰이는 대문자 X(그리스 문자 카이 - 옮긴이)
alert( '😂'.length ); // 2, 웃으면서 눈물 흘리는 얼굴을 나타내는 이모티콘
alert( '𩷶'.length ); // 2, 사용 빈도가 낮은 중국어(상형문자)

 

JS가 만들어졌을 당시엔 서로게이트 쌍은 존재하지 않았습니다. 따라서 JS는 서로게이트

쌍으로 표현한 기호를 제대로 처리하지 못합니다.

위 예시에서 기호는 하나지만 길이는 2인 것을 보고 의아해할 수도 있는데, 이런 이유 때문입니다.

 

String.fromCodePoint와 str.codePointAt 은 명세서에 추가된지 얼마 안 된 메서드로, 서로게이트

쌍을 제대로 처리할 수 있는 몇 안되는 메서드입니다. 두 메서드가 등장하기 전에는 String.fromCharCode와

str.charCodeAt을 사용했었는데, 이 메서드들은 fromCodePoint, codePointAt과 동일하게 동작하지만

서로게이트 쌍은 처리하지 못합니다.

 

서로게이트 쌍은 두 글자로 취급되기 때문에 기호를 가져오는게 꽤 까다롭습니다.

alert( '𝒳'[0] ); // 이상한 기호가 출력됨
alert( '𝒳'[1] ); // 서로게이트 쌍의 일부가 출력됨

 

서로게이트 쌍을 구성하는 글자들은 붙어있을 때만 의미가 있다는 점에 유의해야 합니다.

따라서 위 예시를 실행하면 얼럿창엔 의미 없는 쓰레기 기호가 출력됩니다.

 

기술적으로 서로게이트 쌍은 서로게이트 쌍에 대응하는 코드를 사용해 감지할 수 없습니다.

글자의 코드가 0xd800..0xdbff, 사이에 있으면 이 코드는 서로게이트 쌍을 구성하는 첫번째 글자를 나타낸다는

것을 알 수 있죠. 이 경우 서로게이트 쌍을 구성하는 두번째 글자의 코드는 반드시 0xdc00..0xdfff 사이에

있어야 합니다. 0xd800..0xdbff와 0xdc00..0xdfff는 표준에서 서로게이트 쌍을 위해 일부러 비워둔 코드입니다 .

// charCodeAt는 서로게이트 쌍을 처리하지 못하기 때문에 서로게이트 쌍을 구성하는 부분에 대한 코드를 반환합니다.

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, 0xd800과 0xdbff 사이의 코드
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, 0xdc00과 0xdfff 사이의 코드

발음 구별 기호와 유니코드 정규화

여러 언어에서 베이스가 되는 글자 위나 아래에 발음 구별 기호라 불리는 기호를 붙여 글자를 만듭니다.

a를 베이스 글자로, àáâäãåā 를 만드는 것 같이 말이죠. 이런 '합성' 글자 대부분은 UTF-16테이블에서

독자적인 코드를 갖습니다. 그런데 모든 합성 글자에 코드가 부여되지는 않습니다. 조합 가능한 글자의 수가

너무 많기 때문입니다.

 

임의의 조합을 지원하기 위해 UTF-16에선 몇개의 유니코드 문자를 남겨두었습니다. 베이스 글자 뒤에 하나 혹은

여러 개의 유니코드 문자를 붙여 베이스 글자를 꾸밀 수 있도록 말이죠.

 

이를 이용하면 베이스 글자 S 뒤에 '윗 점'을 나타내는 유니코드 문자 (\n0307)를 붙여 Ṡ를 만들 수 있습니다.

alert( 'S\u0307' ); // Ṡ

발음 구별 기호를 하나 붙인 상태에서 추가 발음 구별 기호가 필요한 경우에도 문제가 없습니다.

필요한 기호의 유니코드 문자를 붙여주기만 하면 됩니다.

 

Ṡ에 '아래 점'을 나타내는 유니코드 문자(\u0323)를 추가해서 'S위와 아라애 점이 붙게' 해봅시다.

예시:

alert( 'S\u0307\u0323' ); // Ṩ

예시:

let s1 = 'S\u0307\u0323'; // Ṩ, S + 윗 점 + 아랫 점
let s2 = 'S\u0323\u0307'; // Ṩ, S + 아랫 점 + 윗 점

alert( `s1: ${s1}, s2: ${s2}` );

alert( s1 == s2 ); // 눈으로 보기엔 같은 글자이지만 동등 비교 시 false가 반환됩니다.

이런 문제를 해결하려면 '유니코드 정규화(unicode normalization)'라 불리는 알고리즘을 사용해

각 문자열을 동일한 형태로 '정규화'해야 합니다.

유니코드 정규화 알고리즘은 str.normalize()에 구현되어 있습니다.

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

S위, 아래에 점을 붙이는 사례에선 normalize()를 사용하면 세 개의 글자가 하나로 합쳐집니다. 

alert( "S\u0307\u0323".normalize().length ); // 1

alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

그런데 현실은 항상 이렇지 않습니다. 

Ṩ가 하나의 유니코드로 합쳐지는 것은 UTF-16을 만드는데 참여한 사람들이 Ṩ는 '충분히 나타낼 수 있는 사례'

라고 생각하고, Ṩ를 UTF-16 테이블에 포함하고 코드를 부여해놓았기 때문입니다.

'JavaScript > Basics' 카테고리의 다른 글

[JavaScript] 배열과 메서드  (0) 2022.03.22
[JavaScript] 배열  (0) 2022.03.22
[JavaScript] 숫자형  (0) 2022.03.16
[JavaScript] 원시값의 메서드  (0) 2022.03.16
[JavaScript] 객체를 원시형으로 변환하기  (0) 2022.03.15