10장 템플릿, 태그 함수, 새로운 문자열 함수

2023.06.16

10.1 템플릿 리터럴

  • ES2015의 템플릿 리터럴(template literal)은 텍스트와 포함된 대체를 결합한 리터럴 구문을 사용하여 문자열(및 기타 엔트리)을 생성하는 방법을 제공한다.
  • 템플릿 리터럴은 백틱(`)으로 구분되며 악센트(accent)라고도 한다.
  • 템플릿 리터럴은 태그가 없는 것과 태그가 있는 것 두 가지 종류가 있다.

10.1.1 기본 기능(태그 없는 템플릿 리터럴)

  • 템플릿 리터럴 내에서 치환자를 사용하여 표현식의 내용을 채울 수 있다.
1const name = 'Fred';
2console.log(`My name is ${name}`); // My name is Fred
3console.log(`Say it loud! ${name.toUpperCase()}!`); // Say it loud! FRED!
  • 태그가 지정되지 않은 템플릿 리터럴에서 표현식의 결과가 문자열이 아니면 문자열로 변환된다.
  • 문자열 리터럴과 달리 템플릿 리터럴에는 이스케이프되지 않은 줄 바꿈이 포함될 수 있으며 템플릿에 유지된다는 것이다.
1console.log(`Line 1
2Line 2`);
3
4// Line1
5// Line2
  • 치환 내용은 자바스크립느 표현식이므로 치환 본문이 복잡한 경우 줄 바꿈과 들여쓰기를 사용할 수 있다. 이는 표현식의 공백일 뿐이므로 문자열에 포함되지 않는다.
1const a = ['one', 'two', 'three'];
2console.log(`Complex: ${a.reverse().join().toUpperCase()}`); // "Complex: THREE,TWO,ONE"

10.1.2 템플릿 태그 함수(태그가 지정된 템플릿 리터럴)

  • 태그 함수는 태그가 지정된 함수 호출 구문을 사용하여 호출하도록 설계된 함수로, 일반 호출처럼 괄호(())를 사용하지 않는다.
1function example(template, ...values) {
2  console.log(template);
3  console.log(values);
4}
5const a = 1,
6  b = 2,
7  c = 3;
8example`Testing ${a} ${b} ${c}.`;
9// =>
10// ["Testing ", " ", " ", "."]
11// [1, 2, 3]
  • 템플릿 배열에는 후행 공백, 세 치환 사이의 공백, 템플릿 끝에 있는 마지막 대체 뒤의 텍스트(마침표)를 포함하는 초기 단어 “testing”이 포함된다.
  • 함수가 고정된 수의 치환자를 예상하지 않는 한, 치환 값에 나머지 매개변수를 사용하는 것이 일반적이다.
  • 평가된 치환 값은 문자열로 변환되지 않고 태그 함수는 실제 값을 가져온다.
  • template에는 최소 하나의 엔트리가 있고 값보다 하나의 엔트리가 더 긴 것이 보장되므로 시드 없는 Array.prototype.reduce는 함께 압축하는 데 적합하다.
1function emulateUntagged(template, ...values) {
2  return template.reduce((acc, str, index) => acc + values[index - 1] + str);
3}
4const a = 1,
5  b = 2,
6  c = 3;
7console.log(emulateUntagged`Testing ${a} ${b} ${c}.`);
8// "Testing 1 2 3."
  • 태그 함수와 템플릿 리터럴을 사용하면 필요한 거의 모든 DSL(도메인 특화 언어)을 만들 수 있다.
    • 정규표현식이 좋은 예이다.
    • 문자열을 받기 때문에 RegExp 생성자를 사용하는 것이 어색하고 정규 표현식에 대한 모든 백슬래시는 이스케이프되어야 한다.
1function example(template) {
2  const first = template.raw[0];
3  console.log(first);
4  console.log(first.length);
5  console.log(first[0]);
6}
7example`\u000A\x0a\n`;
  • 템플릿 배열에 raw 속성은 템플릿 텍스트 세그먼트에 대한 미가공 텍스트 배열을 포함한다.
  • 위의 코드에서 백슬래시는 실제로 백슬래시이고 이스케이프 시퀀스가 해석되지 않았다. 템플릿 리터럴에 작성된 대로다.
1const createRegex = (template, ...values) => {
2  // Build the source from the raw text segments and values
3  // (in a later section, you'll see something that can replace
4  // this reduce call)
5  const source = template.raw.reduce((acc, str, index) => acc + values[index - 1] + str);
6  // Check it's in /expr/flags form
7  const match = /^\/(.+)\/([a-z]*)$/.exec(source);
8  if (!match) {
9    throw new Error('Invalid regular expression');
10  }
11  // Get the expression and flags, create
12  const [, expr, flags = ''] = match;
13  return new RegExp(expr, flags);
14};
15
16// From the TC39 proposal: https://github.com/benjamingr/RegExp.escape
17const escapeRegExp = (s) => String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
18
19const alternatives = ['this', 'that', 'the other'];
20const rex = createRegex`/\b(?:${alternatives.map(escapeRegExp).join('|')})\b/i`;
21
22const test = (str, expect) => {
23  const result = rex.test(str);
24  console.log(str + ':', result, '=>', !result == !expect ? 'Good' : 'ERROR');
25};
26test("doesn't have either", false);
27test('has_this_but_not_delimited', false);
28test('has this ', true);
29test('has the other ', true);
  • 위의 태그 함수를 사용하면 엔트리를 이중으로 이스케이프하지 않고도 포함된 변수가 있는 정규표현식을 작성할 수 있다.

10.1.3 String.raw

  • 태그 함수로 사용될 때 String.raw는 평가된 치환 값과 결합된 템플릿의 raw 텍스트 세그먼트가 있는 문자열을 반환한다.
  • 문자열의 이스케이프 시퀀스를 해석하지 않고 문자열을 생성하고자 할 때 유용하다.
    • 윈도 컴퓨터의 유틸리티 스크립트에서 하드코딩된 경로 지정
    1fs.open(String.raw`C:\nifty\stuff.json`);
    • 백슬레시와 변수 부분을 포함하는 정규 표현식 만들기
    1new RegExp(String.raw`^\d+${separator}\d+$`);
    • 다른 태그 함수에서 사용할 때도 유용하다.
    1const createRegex = (template, ...values) => {
    2  const source = String.raw(template, ...values);
    3  // do something
    4};
  • 기본적으로 String.raw는 해석된 문자열이 아니라 입력한 미가공 문자열(대체 가능)을 원할 때마다 유용하다.

10.1.4 템플릿 리터럴 재사용하기

  • 템플릿 재사용을 원한다면 함수로 래핑할 수 있다.
1const formatUser = (firstName, lastName, handle) => `${firstName} ${lastName} (${handle})`;
2console.log(formatUser('Joe', 'Bloggs', '@joebloggs'));

10.1.5 템플릿 리터럴과 자동 세미콜론 삽입

  • 세미콜론 없이 코드를 작성한다면 (나, [ 줄을 시작하는 것을 피하는 데 익숙할 것이다. 이것은 잘못된 함수 호출이나 배열의 시작을 의미하는 속성 접근자와 같이 의도하지 않은 동작을 유발할 수 있다.
  • 템플릿 리터럴은 새로운 ASI (Automatic Semicolon Insertion) 위험을 추가한다.
  • 템플릿 리터럴을 시작하기 위해 백틱으로 줄을 시작하면 이전 줄의 끝에서 참조되는 함수에 대한 태그 호출로 볼 수 있다. ( ( 처럼 )

10.2 향상된 유니코드 지원

10.2.1 유니코드와 자바스크립트 문자열은 무엇일까?

  • 유니코드와 유니코드 변환 양식(Unicode Transformation Format, UTF), 코드포인트, 코드 단위 등에 대해 확실히 이해하고 있다면 자바스크립트 문자열은 유효하지 않은 대리 쌍(surrogate pair)을 허용하는 일련의 UTF-16 코드 단위로 볼 수 있다.
  • 복잡한 인간의 언어를 위해 유니코드는 코드 포인트, 0x000000(0)에서 0x10FFFF(1,114,111) 범위의 값을 특정 의미와 속성으로 정의하며 일반적으로 “U+” 다음에 4~6자리의 16진수로의 작성된다.
  • 코드 포인트는 그 자체로 “문자(eg. 영어 a)”이거나 “기본 문자(eg. नि, 데바나가리 문자의 na 음절)” 또는 “결합”일 수 있다.
  • 현대 시스템은 16비트 “문자를” 사용하여 문자열을 저장했고 유니코드를 16비트 이상으로 확장해야 하는 경우
  • 16비트 값을 코드 포인트와 구별하기 위해 코드 단위라고 한다.
  • 21비트 코드 포인트 값을 16비트 코드 단위 값으로 변환하는 것을 UTF-16이라고 한다.
  • 잘 구성된 UTF-16에서는 선행 써로게이트(surrogate)가 없고 뒤에 후행 써로게이트가 오지 않거나 그 반대의 경우도 마찬가지다.
  • 인간의 관점에서 볼 때 단일 자소가 하나 이상의 코드포인트일 수 있고, 단일 코드 포인트가 하나 또는 두개의 UTF-16 코드 단위(자바스크립트 문자열 “문자”)일 수 있음을 의미한다.
  • 코드 포인트와 코드 단위 예
“문자”코드 포인트UTF-16 코드 단위
영어 “a”U+00610061
데바나가리 निU+0928 U+093F0928 093F
스마일 이모지 😊U+1F60AD83D DE0A
  • 영어 자소가 “a”가 단일 코드 포인트이고 단일 UTF-16 코드 단위
  • 데바나가리 “नि”은 두 개의 코드 포인트가 필요하며, 각각은 단일 UTF-16 코드 단위
  • 이모지 😊는 단일 코드 포인트이지만 두 개의 UTF-16 코드 단위와 두 개의 자바스크립트 “문자”가 필요
1console.log('a'.length); // 1
2console.log('नि'.length); // 2
3console.log('😊'.length); // 2

10.2.2 코드 포인트 이스케이프 시퀀스

  • ES2015에는 유니코드 코드 포인트 이스케이프 시퀀스가 추가되어 실제 코드 포인트 값을 지정할 수 있다.
1// UTF-16 값을 파악한 다음 작성
2console.log('\uD83D\uDE0A'); // 😊
3
4// 유니코드 코드 포인트 이스케이프 시퀀스 사용
5console.log('\u{1F60A}'); // 😊

10.2.3 String.fromCodePoint

  • ES2015는 또한 String.fromCode(코드 단위로 작동)에 해당하는 코드 포인트인 String.fromCodePoint를 추가했다.
1console.log(String.fromCodePoint(0x1f60a)); //😊

10.2.4 String.prototype.codePointAt

  • String.prototype.codePointAt을 통해 문자열의 주어진 위치에서 코드 포인트를 얻을 수 있다.
1console.log('😊'.codePointAt(0).toString(16).toUpperCase()); // 1F60A
  • 전달하는 인덱스는 코드 포인트가 아니라 코드 단위(자바스크립트의 “문자”)다.
    • 따라서, s.codePointAt(1)은 문자열의 두 번째 코드 포인트를 반환하지 않고 문자열의 인덱스 1에서 시작하는 코드 포인트를 반환한다.
    • 문자열의 첫 번쨰 코드 포인트에 두 개의 코드 단위가 필요한 경우 s.codePointAt(1)은 해당 코드 포인트의 후행 써로게이트 코드 단위 값을 반환한다.
1const charToHex = (str, i) => '0x' + str.codePointAt(i).toString(16).toUpperCase().padStart(6, '0');
2const str = '😊😊'; // Two identical smiling face emojis
3for (let i = 0; i < str.length; ++i) {
4  console.log(charToHex(str, i));
5}
6// =>
7// 0x01F60A
8// 0x00DE0A
9// 0x01F60A
10// 0x00DE0A
  • 위 코드에서 각각의 웃는 얼굴이 문자열에서 두 개의 “문자”를 차지하기 때문에 네 개의 값을 출력하지만 코드는 각 반복에서 카운터를 하나씩만 진행한다.

10.2.5 String.prototype.normalize

  • normalize 메서드는 유니코드 컨소시엄에서 정의한 정규화 양식 중 하나를 사용하여 새로운 “정규화된” 문자열을 만든다.
1const f1 = 'Français';
2const f2 = 'Franc\u0327ais';
3
4console.log(f1); // Français
5console.log(f2); // Français
6console.log(f1 === f2); // false
7console.log(f1.normalize() === f2.normalize()); // true
  • 프랑스어에는 “c”에 갈고리형 발음 기호가 있고, f1과 f2는 동일한 단어이지만 단순 비교를 하면 다르다는 결과를 얻는다. 정규화는 이것을 해결한다.
  • 일부 언어는 동일한 “문자”에 여러 발음 구별 기호를 적용할 수 있다.
  • 한 문자열과 다른 문자열에서 표시 순서가 다른 경우 문자열은 단순 검사에서 동일하지 않지만 정규화된 양식에서는 동일하다.

10.3 반복

  • ES2015부터 문자열은 이터러블이다. 반복은 문자열의 각 코드 포인트(각 코드 단위가 아닌)를 방문한다.
1for (const ch of '>😊<') {
2  console.log(`${ch} (${ch.length})`);
3}
4// =>
5// > (1)
6// 😊 (2)
7// < (1)
  • ES2015 이전의 관용적 방법은 문자열을 코드 단위의 배열로 분할하는 str.split(””)을 사용하는 것이었다.
  • ES2015부터 Array.from(str)을 대신 사용하도록 선택할 수 있으며, 코드 포인트의 배열(코드 단위가 아님)이 생성된다.
1const charToHex = (ch) => '0x' + ch.codePointAt(0).toString(16).toUpperCase().padStart(6, '0');
2const show = (array) => {
3  console.log(array.map(charToHex));
4};
5
6const str = '>😊<';
7show(str.split('')); // ["0x00003E", "0x00D83D", "0x00DE0A", "0x00003C"]
8show(Array.from(str)); // ["0x00003E", "0x01F60A", "0x00003C"]

10.4 새로운 문장열 메서드

10.4.1 String.prototype.repeat

  • repeat은 단순히 주어진 횟수만큼 호출한 문자열을 반복한다.
1console.log('n'.repeat(3)); // nnn

10.4.2 String.prototype.startsWith, endsWidth

  • startsWith 및 endsWith는 문자열이 부분 문자열로 시작하거나 끝나는지 여부를 확인하는 간단한 방법을 제공한다.
  • startsWith 시작 인덱스를 전달하면 문자열이 해당 인덱스에서 시작된 것처럼 처리한다.
  • endsWith 종료 인덱스를 전달하면 문자열이 해당 인덱스에서 끝난 것처럼 처리한다.
1console.log('testing'.startsWith('test')); // true
2console.log('testing'.endsWith('ing')); // true
3console.log('testing'.endsWith('foo')); // false
4
5console.log('now testing'.startsWith('test')); // false
6console.log('now testing'.startsWith('test', 4)); // true
7// Index 4 ------^
8
9console.log('now testing'.endsWith('test')); // false
10console.log('now testing'.endsWith('test', 8)); // true
11// Index 8 ----------^

10.4.3 String.prototype.includes

  • includes는 호출한 문자열에 전달한 부분 문자열이 포함되어 있는지 확인하고 선택적으로 문자열의 지정된 위치에서 시작하는지 확인한다.
1console.log('testing'.includes('test')); // true
2console.log('testing'.includes('test', 1)); // false

10.4.4 String.prototype.padStart, padEnd

  • ES2017은 padStart와 padEnd를 통해 표준 라이브러리에 문자열 패딩을 추가했다.
  • 원하는 문자열의 전체 길이를 지정하고 선택적으로 패딩에 사용할 문자열(기본값은 공백)을 지정한다.
    • 원하는 패딩의 양이 아니라 결과 문자열의 총 길이를 지정한다는 점에 유의하자.
1const s = 'example';
2console.log(`|${s.padStart(10, '-')}|`);
3// => "|---example|"
4console.log(`|${s.padEnd(10, '-')}|`);
5// => "|example---|"
6
7const s = 'example';
8console.log(`|${s.padStart(10, '-*')}|`);
9// => "|-*-example|"
10console.log(`|${s.padEnd(10, '-*')}|`);
11// => "|example-*-|"
12console.log(`|${s.padStart(14, '...oooOOO')}|`);
13// => "|...oooOexample|"

10.4.5 String.prototype.trimStart, trimEnd

  • ES2019는 문자열에 trimStart와 trimEnd를 추가했다.
  • 각각 문자열의 시작 부분 공백과, 끝 부분 공백을 제거한다.
1const s = '    testing    ';
2const startTrimmed = s.trimStart();
3const endTrimmed = s.trimEnd();
4console.log(`|${startTrimmed}|`);
5// => |testing    |
6console.log(`|${endTrimmed}|`);
7// => |    testing|

10.5 match, split, search, replace 메서드 업데이트

  • ES2015 이전에는 정규 표현식과 match, search, split, replace가 매우 밀접하게 연결되어 있었지만, ES2015에서 더 일반화되었다.
  • ES2015부터는 match, search, split, replace와 함께 사용할 고유한 객체를 생성할 수 있으며 메서드가 특정이름을 가진 메서드에서 찾는 특정 기능이 있는 경우 해당 메서드를 객체에 전달할 수 있다.
    • match: Symbol.match를 찾음
    • split: Symbol.split를 찾음
    • search: Symbol.search를 찾음
    • replace: Symbol.replace를 찾음
1// String.prototype 내부
2split(separator) {
3    if (separator !== undefined && separator !== null) {
4        if (separator[Symbol.split] !== undefined) {
5            return separator[Symbol.split](this);
6        }
7    }
8
9    const s = String(separator);
10    const a = [];
11    // ... `s`에서 문자열을 분할하고 `a`에 추가...
12    return a;
13}
  • ES2015에서 split 메서드는 개념적으로 위와 같다.
  • String.prototype.split은 매개 변수의 Symbolsplit 메서드가 있는 경우 해당 메서드를 전달하고 그렇지 않으면 과거에 비정규 표현식 구분 기호로 수행했던 작업을 수행한다.

10.6 과거 습관을 새롭게

10.6.1 문자열 연결 대신 템플릿 리터럴 사용(적절한 경우)

  • 스타일의 문제일 수 있지만 대신 템플릿 리터럴을 사용하자.
1const formatUserName = user => {
2    return `${user.firstName} ${user.lastName} (${user.handle});
3};

10.6.2 커스텀 플레이스 홀더 메커니즘 대신 DSL을 위해 태그 함수와 템플릿 리터럴 사용

  • 의미가 있는 상황에서는 템플릿에서 제공하는 대체 평가를 활용하여 태그 기능과 템플릿 리터럴을 사용하자.

10.6.3 문자열 이터레이터 사용

  • 문자열을 코드 단위가 아닌 일련의 코드 포인트로 처리하려면 codePointAt 또는 for-of 또는 기타 유니코드 인식 기능을 사용하자.