19일 7월 2020

문자열

자바스크립트엔 글자 하나만 저장할 수 있는 별도의 자료형이 없습니다. 텍스트 형식의 데이터는 길이에 상관없이 문자열 형태로 저장됩니다.

자바스크립트에서 문자열은 페이지 인코딩 방식과 상관없이 항상 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

\n은 ‘특수 문자’ 하나로 취급되기 때문에 My\n의 길이는 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) 메서드를 이용하는 것입니다.

이 메서드는 문자열 strpos에서부터 시작해, 부분 문자열 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문에선 0false로 간주하므로 alert 창이 뜨지 않습니다.

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

let str = "Widget with id";

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

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

오래전부터 전해 오는 비트(bitwise) NOT 연산자 ~를 사용한 기법 하나를 소개해드리겠습니다. 비트 NOT 연산자는 피연산자를 32비트 정수로 바꾼 후(소수부는 모두 버려짐) 모든 비트를 반전합니다.

따라서 n이 32비트 정수일 때 ~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 중, ~n0으로 만드는 경우는 n == -1일 때가 유일합니다.

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

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

let str = "Widget";

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

사실 이렇게 언어 특유의 기능을 사용해 직관적이지 않은 코드를 작성하는 것을 추천해 드리진 않습니다. 그렇지만 위와 같은 기법은 오래된 스크립트에서 쉽게 만날 수 있기 때문에 알아두어야 합니다.

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

참고로 -1 이외에도 ~ 연산자 적용 시 0을 반환하는 숫자는 다양합니다. 아주 큰 숫자에 ~ 연산자를 적용하면 32비트 정수로 바꾸는 과정에서 잘림 현상이 발생하기 때문이죠. 이런 숫자 중 가장 큰 숫자는 4294967295입니다(~42949672950임). 문자열이 아주 길지 않은 경우에만 ~ 연산자가 의도한 대로 작동한다는 점을 알고 계시길 바랍니다.

모던 자바스크립트에선 .includes 메서드(아래에서 배움)를 사용해 부분 문자열 포함 여부를 검사합니다. 이런 기법은 오래된 자바스크립트에서만 볼 수 있습니다.

includes, startsWith, endsWith

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

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

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.startsWithstr.endsWith는 메서드 이름 그대로 문자열 str이 특정 문자열로 시작하는지(start with) 여부와 특정 문자열로 끝나는지(end with) 여부를 확인할 때 사용할 수 있습니다.

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

부분 문자열 추출하기

자바스크립트엔 부분 문자열 추출과 관련된 메서드가 세 가지 있습니다. 세 가지 메서드 substring, substr, slice를 하나씩 알아봅시다.

str.slice(start [, 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번째부터 끝까지

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

let str = "stringify";

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

startend 사이에 있는 문자열을 반환합니다.

substringslice와 아주 유사하지만 startend보다 커도 괜찮다는 데 차이가 있습니다.

예시:

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은 끝 위치 대신에 길이를 기준으로 문자열을 추출한다는 점에서 substringslice와 차이가 있습니다.

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) startend 사이 음수는 0으로 취급함
substr(start, length) start부터 length개의 글자 음수 허용
어떤 메서드를 선택해야 하나요?

모두 사용해도 괜찮습니다. 그런데 substr에는 단점이 하나 있습니다. substr는 코어 자바스크립트 명세서(ECMA-262 – 옮긴이)가 아닌, 구식 스크립트에 대응하기 위해 남겨 둔 브라우저 전용 기능들을 명시해 놓은 부록 B(Annex B)에 정의되어있습니다. 거의 모든 곳에서 이 메서드가 동작하긴 하지만 브라우저 이외의 호스트 환경에서는 제대로 동작하지 않을 수 있습니다.

남은 두 메서드 중 slice는 음수 인수를 허용한다는 측면에서 substring보다 좀 더 유연합니다. 메서드 이름도 더 짧죠. 따라서 세 메서드 중 slice만 외워놓고 사용해도 충분할 것 같습니다.

문자열 비교하기

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

그런데 아래와 같이 몇 가지 이상해 보이는 것들이 있습니다.

  1. 소문자는 대문자보다 항상 큽니다.

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

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

    이런 예외사항 때문에 이름순으로 국가를 나열할 때 예상치 못한 결과가 나올 수 있습니다. 사람들은 ÖsterreichZealand보다 앞서 나올 것이라 예상하는데 그렇지 않죠.

자바스크립트 내부에서 문자열이 어떻게 표시되는지 상기하며 원인을 알아봅시다.

모든 문자열은 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

이제 이 배경지식을 가지고 코드 65220 사이(라틴계열 알파벳과 기타 글자들이 여기에 포함됨)에 대응하는 글자들을 출력해봅시다.

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에서 정의한 규칙에 따라 strstr2보다 작은지, 같은지, 큰지를 나타내주는 정수가 반환됩니다.

  • strstr2보다 작으면 음수를 반환합니다.
  • strstr2보다 크면 양수를 반환합니다.
  • strstr2이 같으면 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, 사용 빈도가 낮은 중국어(상형문자)

자바스크립트가 만들어졌을 당시엔 서로게이트 쌍은 존재하지 않았습니다. 따라서 자바스크립트는 서로게이트 쌍으로 표현한 기호를 제대로 처리하지 못합니다.

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

String.fromCodePointstr.codePointAt은 명세서에 추가된 지 얼마 안 된 메서드로, 서로게이트 쌍을 제대로 처리할 수 있는 몇 안 되는 메서드 입니다. 두 메서드가 등장하기 전에는 String.fromCharCodestr.charCodeAt을 사용했었는데, 이 메서드들은 fromCodePoint, codePointAt과 동일하게 동작하지만 서로게이트 쌍은 처리하지 못합니다.

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

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

서로게이트 쌍을 구성하는 글자들은 붙어있을 때만 의미가 있다는 점에 유의해야 합니다. 따라서 위 예시를 실행하면 얼럿창엔 의미 없는 쓰레기 기호가 출력됩니다.

기술적으로 서로게이트 쌍은 서로게이트 쌍에 대응하는 코드를 사용해 감지할 수 있습니다. 글자의 코드가 0xd800..0xdbff 사이에 있으면 이 코드는 서로게이트 쌍을 구성하는 첫 번째 글자를 나타낸다는 것을 알 수 있죠. 이 경우 서로게이트 쌍을 구성하는 두 번째 글자의 코드는 반드시 0xdc00..0xdfff 사이에 있어야 합니다. 범위 0xd800..0xdbff0xdc00..0xdfff는 표준에서 서로게이트 쌍을 위해 일부러 비워둔 코드입니다.

예시를 살펴봅시다.

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

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

서로게이트 쌍을 다루는 다양한 방법에 대해선 iterable 객체 챕터에서 살펴보겠습니다. 서로게이트 쌍 관련 라이브러리도 있긴 한데 소개해 드릴 만한 라이브러리는 아직까진 없는 상황입니다.

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

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

a를 베이스 글자로, àáâäãåā를 만드는 것 같이 말이죠. 이런 ‘합성’ 글자 대부분은 UTF-16 테이블에서 독자적인 코드를 갖습니다. 그런데 모든 합성 글자에 코드가 부여되지는 않습니다. 조합 가능한 글자의 수가 너무 많기 때문입니다.

임의의 조합을 지원하기 위해 UTF-16에선 몇 개의 유니코드 문자를 남겨두었습니다. 베이스 글자 뒤에 하나 혹은 여러 개의 유니코드 문자를 붙여 베이스 글자를 꾸밀 수 있도록 말이죠.

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

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()를 사용하면 세 개의 글자가 하나로 합쳐집니다. 를 나타내는 유니코드 \u1e68로 말이죠.

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

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

그런데 현실은 항상 이렇지 않습니다. 가 하나의 유니코드로 합쳐지는 것은 UTF-16을 만드는 데 참여한 사람들이 는 '충분히 나타날 수 있는 사례’라 생각하고, 를 UTF-16 테이블에 포함하고 코드를 부여해놓았기 때문입니다.

실무에선 이 절에서 다룬 내용만으로도 충분하지만, 정규화 규칙과 변형에 대해 더 알고 싶다면 유니코드 표준 부록의 Unicode Normalization Forms에 해당 내용이 있으니 참고하시기 바랍니다.

요약

  • 자바스크립트엔 세 종류의 따옴표가 있는데, 이 중 하나인 백틱은 문자열을 여러 줄에 걸쳐 쓸 수 있게 해주고 문자열 중간에 ${…}을 사용해 표현식도 넣을 수 있다는 점이 특징입니다.
  • 자바스크립트에선 UTF-16을 사용해 문자열을 인코딩합니다.
  • \n 같은 특수 문자를 사용할 수 있습니다. \u...를 사용하면 해당 문자의 유니코드를 사용해 글자를 만들 수 있습니다.
  • 문자열 내의 글자 하나를 얻으려면 대괄호 []를 사용하세요.
  • 부분 문자열을 얻으려면 slicesubstring을 사용하세요.
  • 소문자로 바꾸려면 toLowerCase, 대문자로 바꾸려면 toUpperCase를 사용하세요.
  • indexOf를 사용하면 부분 문자열의 위치를 얻을 수 있습니다. 부분 문자열 여부만 알고 싶다면 includes/startsWith/endsWith를 사용하면 됩니다.
  • 특정 언어에 적합한 비교 기준 사용해 문자열을 비교하려면 localeCompare를 사용하세요. 이 메서드를 사용하지 않으면 글자 코드를 기준으로 문자열이 비교됩니다.

이외에도 문자열에 쓸 수 있는 유용한 메서드 몇 가지가 있습니다.

  • str.trim() – 문자열 앞과 끝의 공백 문자를 다듬어 줍니다(제거함).
  • str.repeat(n) – 문자열을 n번 반복합니다.
  • 이 외의 메서드는 MDN 문서에서 확인해보시기 바랍니다.

정규 표현식을 사용해 문자열을 찾거나 교체해주는 메서드도 여러 개 있는데 이는 아주 큰 주제이기 때문에 별도의 섹션 정규 표현식에서 다루겠습니다.

과제

중요도: 5

str의 첫 글자를 대문자로 바꿔 반환하는 함수, ucFirst(str)를 만들어보세요. 함수 실행 결과는 아래 예시를 충족해야 합니다.

ucFirst("john") == "John";

테스트 코드가 담긴 샌드박스를 열어 정답을 작성해보세요.

자바스크립트에서 문자열은 수정할 수 없기 때문에 단순히 첫 글자만 바꾸는 것은 불가능합니다.

기존 문자열을 참고해 첫 글자만 대문자로 바꿔 새로운 문자열을 만들어야 우리가 원하는 작업을 할 수 있습니다. 아래와 같이 말이죠.

let newStr = str[0].toUpperCase() + str.slice(1);

그런데 이렇게 코드를 작성하면 str이 비어있는 문자열인 경우 str[0]undefined가 되는 문제가 발생합니다. undefinedtoUpperCase()메서드를 지원하지 않으므로 에러가 발생하죠.

두 가지 방법을 사용해 이런 예외사항을 처리 할 수 있습니다.

  1. str.charAt(0)str이 비어있는 문자열이더라도 항상 문자열을 반환하므로, 이 메서드를 사용합니다.
  2. 빈 문자열인지를 확인하는 코드를 작성합니다.

두 번째 방법을 사용하여 작성한 답안은 아래와 같습니다.

function ucFirst(str) {
  if (!str) return str;

  return str[0].toUpperCase() + str.slice(1);
}

alert( ucFirst("john") ); // John

테스트 코드가 담긴 샌드박스를 열어 정답을 확인해보세요.

중요도: 5

str에 'viagra’나 'XXX’라는 문자열이 있으면 true를 반환해주는 함수 checkSpam(str)을 만들어보세요. 해당 문자열이 없으면 false를 반환하면 됩니다.

함수는 대·소문자 관계없이 해당 단어를 걸러주어야 합니다.

checkSpam('buy ViAgRA now') == true
checkSpam('free xxxxx') == true
checkSpam("innocent rabbit") == false

테스트 코드가 담긴 샌드박스를 열어 정답을 작성해보세요.

대·소문자 관계없이 스팸 문자열을 확인하려면 인수로 받은 문자열을 구성하는 문자를 모두 소문자로 바꾼 후 작업해야 합니다.

function checkSpam(str) {
  let lowerStr = str.toLowerCase();

  return lowerStr.includes('viagra') || lowerStr.includes('xxx');
}

alert( checkSpam('buy ViAgRA now') );
alert( checkSpam('free xxxxx') );
alert( checkSpam("innocent rabbit") );

테스트 코드가 담긴 샌드박스를 열어 정답을 확인해보세요.

중요도: 5

str의 길이를 확인하고, 최대 길이 maxlength를 초과하는 경우 str의 끝을 생략 부호 ("…")로 대체해주는 함수 truncate(str, maxlength)를 만들어봅시다. 새로 만든 문자열의 길이는 maxlength가 되어야 합니다.

함수의 반환 값은 원하는 길이로 줄여진 문자열이 되어야 합니다.

예시:

truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…"

truncate("Hi everyone!", 20) = "Hi everyone!"

테스트 코드가 담긴 샌드박스를 열어 정답을 작성해보세요.

새로 만든 문자열의 길이는 maxlength가 되어야 하므로, 생략 부호 "…"가 차지할 길이를 생각하여 함수를 만들어야 합니다.

생략 부호는 유니코드에 등록된 독립된 글자임에 유의하여 답안을 작성해야 합니다. 점 세 개가 아님에 유의하시기 바랍니다.

function truncate(str, maxlength) {
  return (str.length > maxlength) ?
    str.slice(0, maxlength - 1) + '…' : str;
}

테스트 코드가 담긴 샌드박스를 열어 정답을 확인해보세요.

중요도: 4

달러 표시가 먼저 나오고 그 뒤에 숫자가 나오는 문자열 "$120"가 있다고 가정해 봅시다.

위와 같은 문자열에서 숫자만 뽑아내는 함수 extractCurrencyValue(str)를 작성해 봅시다.

실행 결과는 아래와 같아야 합니다.

alert( extractCurrencyValue('$120') === 120 ); // true

테스트 코드가 담긴 샌드박스를 열어 정답을 작성해보세요.

function extractCurrencyValue(str) {
  return +str.slice(1);
}

테스트 코드가 담긴 샌드박스를 열어 정답을 확인해보세요.

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.