ES6 In Depth: 템플릿 문자열 (Template string)

ES6 In Depth는 ECMAScript 표준 6번째 에디션(줄여서 ES6)을 맞아 JavaScript에 새로 추가된 요소들을 살펴보는 시리즈입니다.

저는 지난주에 학습 페이스를 조절하기로 약속했습니다. 이터레이터(iterator)제너레이터(generator) 다음으로 약간 쉬운 주제를 다루자고 했습니다. 우리 두뇌를 혹사시키지 않을 주제 말입니다. 이 글 마지막에서 제가 약속을 지켰는지 평가해 주세요.

일단, 간단한 내용으로 시작합니다.

백틱(Backtick)의 기초

ES6는 템플릿 문자열(template string)이라고 불리는 새로운 종류의 문자열 표기법을 도입합니다. 템플릿 문자열은 일반 문자열과 비슷해 보이지만, '" 같은 통상적인 따옴표 문자 대신 백틱(backtick) 문자 `를 사용합니다. 가장 간단하게 사용할 경우, 템플릿 문자열은 정말 일반 문자열과 똑같습니다.

context.fillText(`Ceci n'est pas une chaîne.`, x, y);

하지만 이것을 “백틱을 써서 표기하는 것 말고는 특별한 것이 없는 늘상 봐오던 평범한 문자열”이라고 부르지 않고 “템플릿 문자열”이라고 부르는 데는 이유가 있습니다. 템플릿 문자열 덕분에 JavaScript에 간단한 문자열 채워넣기 (string interpolation) 기능이 가능해졌습니다. 템플릿 문자열은 JavaScript 값(value)을 문자열에 끼워넣는 보기 좋고 편리한 방법입니다.

템플릿 문자열의 용도는 수백만 가지입니다. 하지만 저는 다음 같은 소박한 에러 메시지 처리만으로도 마음이 따뜻해집니다.

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      `User ${user.name} is not authorized to do ${action}.`);
  }
}

이 예제에서 ${user.name}${action}템플릿 대입문(template substitution)이라고 부릅니다. JavaScript는 user.name 값과 action 값을 결과 문자열에 대입합니다. 그래서 User jorendorff is not authorized to do hockey. 같은 문자열이 생성됩니다 (이 메시지는 참입니다. 저는 하키 면허를 갖고 있지 않습니다.)

여기까지는 + 연산자보다 약간 더 좋은 문법일 뿐입니다. 다음과 같은 상세한 동작 양식도 충분히 예측 가능할 것입니다.

  • 템플릿 대입문에는 어떤 JavaScript 표현도 올 수 있습니다. 함수 호출 구문, 수식 구문 등 모든 구문이 허용됩니다. (만약 원한다면 템플릿 문자열을 다른 템플릿 문자열 안에 포함시키는 것도 가능합니다. 저는 그것을 템플릿 인셉션이라고 부릅니다.)
  • 만약 템플릿 대입문에 오는 값이 문자열이 아니라면 그 값은 통상적인 규칙에 따라 문자열로 변환될 것입니다. 위 예제 코드에서 만약 action이 객체라면 해당 객체의 .toString() 메소드가 호출될 것입니다.
  • 템플릿 문자열 안에서 백틱 문자를 써야할 필요가 있다면 백슬래쉬 문자를 이용한 이스케이프(escape) 표현을 사용합니다. `\``라고 표현하는 것은 "`"라고 표현하는 것과 동일합니다.
  • 마찬가지로, 어떤 이유일지는 모르겠지만 템플릿 문자열 안에서 ${ 2개 문자를 사용해야 한다면 2개 중 한 문자를 백슬래쉬 문자로 이스케이프시켜 표현합니다. `write \${ or $\{`.

일반적인 문자열과 달리 템플릿 문자열은 여러 줄에 걸쳐 표현할 수 있습니다.

$("#warning").html(`
  <h1>Watch out!</h1>
  <p>Unauthorized hockeying can result in penalties
  of up to ${maxPenalty} minutes.</p>
`);

줄바꿈과 들여쓰기 등 템플릿 문자열 속의 모든 화이트 스페이스들은 있는 그대로 포함됩니다.

그렇습니다. 지난주에 약속한 것이 있기 때문에 저는 당신의 두뇌에 책임을 느낍니다. 그래서 짧게 경고하려고 합니다. 이제부터 약간 어려워질 것입니다. 잠깐 멈추고 커피 한잔 하셔도 좋습니다. 기민하고 맑은 두뇌를 위해서요. 정말입니다. 쉬었다 돌아오는 것을 부끄럽게 생각하지 마세요. 사상 최초로 적도를 횡단한 Lopes Gonçalves가 바다 괴물에게 공격당하거나 세상 끝 낭떠러지에서 추락하는 일 없이 자기 배로 무사히 적도를 처음 건넜을 때, 그가 쉬지 않고 남반구 전부를 탐험했던가요? 아닙니다. 그는 그냥 돌아왔습니다. 집으로 돌아와서 좋은 점심을 먹었습니다. 점심 좋아하시죠?

백틱의 미래

템플릿 문자열이 하지 못하는 일들을 이야기해 봅시다.

  • 템플릿 문자열은 특수 문자들을 자동으로 이스케이프시켜 표현해주지 않습니다. 크로스-사이트 스크립팅(cross-site scripting) 공격을 피하려면, 신뢰할 수 없는 데이터에 대한 처리를 직접해주어야 합니다. 처리 방식은 일반 문자열을 처리할 때와 같은 방식입니다.
  • 템플릿 문자열과 다국어 처리 라이브러리(internationalization library) (당신의 코드가 다양한 사용자들을 위해 다양한 언어를 말할 수 있도록 도와주는 라이브러리) 사이의 상호작용에 대해서는 분명히 정의된 것이 없습니다. 템플릿 문자열은 언어권별 숫자 포맷과 날짜 포맷을 알아서 처리해주지 않습니다. 복수형 처리는 말할 것도 없구요.
  • 템플릿 문자열은 MustacheNunjucks 같은 템플릿 라이브러리의 대체품이 아닙니다.
    템플릿 문자열은 루프문은 물론 조건문을 위한 문법을 갖고 있지 않습니다. 루프문은 예를 들어 배열로 HTML 테이블을 만들 때 사용합니다. (그렇습니다. 이를 위해 이른바 템플릿 인셉션을 이용할 수도 있을 것입니다. 하지만 그건 농담으로나 해 볼 만한 무리한 시도입니다.)

ES6는 템플릿 문자열의 쓰임새를 높이는 방법을 하나 더 제공합니다. 이 방법을 이용하면 JS 개발자들과 라이브러리 설계자들이 템플릿 문자열의 한계를 극복할 수 있습니다. 태그된 템플릿(tagged template)이라고 불리는 방법입니다.

태그된 템플릿의 문법은 간단합니다. 태그된 템플릿은 단지 시작하는 백틱 앞에 태그(tag)를 하나 더 붙인 템플릿 문자열입니다. 우선 처음 볼 예제의 태그 이름을 SaferHTML로 합시다. 우리는 이 태그를 이용해서 앞에서 나열한 템플릿 문자열의 한계들 중 첫번째 한계를 극복할 것입니다. 특수 문자들을 자동으로 이스케이프시켜 표현하는 것 말입니다.

한가지 일러둘 것은 SaferHTML 태그는 ES6 표준 라이브러리가 제공하는 특별한 키워드가 아닙니다. 우리는 이제부터 SaferHTML 태그 키워드를 직접 구현할 것입니다.

var message =
  SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`;

여기서 사용된 SaferHTML 태그는 단순한 형태의 식별자입니다. 하지만 SaferHTML.escape 같은 속성 형태도 태그로 사용할 수 있습니다. 그리고 SaferHTML.escape({unicodeControlCharacters: false}) 같은 메소드 호출 형태도 태그로 사용할 수 있습니다. (정확히 말하자면, 모든 ES6 MemberExpression 또는 CallExpression을 태그로 사용할 수 있습니다.)

앞서 우리는 태그 없이 사용된 템플릿 문자열이 문자열을 조합하는 작업에 대한 약식 표기임을 보았습니다. 태그된 템플릿(tagged template)은 완전히 다른 작업에 대한 약식 표기입니다. 바로 함수 호출입니다.

위의 코드는 다음 코드와 완전히 동일합니다.

var message =
  SaferHTML(templateData, bonk.sender);

여기서 templateData는 템플릿의 문자열 파트로 이루어진, 값을 변경할 수 없는 배열입니다. templateData 배열은 JS 엔진에 의해 만들어집니다. 이 예제에서 templateData 배열은 2개의 요소를 갖습니다. 태그된 템플릿(tagged template)이 대입문(substitution)에 의해 분리된 2개의 문자열 파트로 구성되기 때문입니다. 그래서 templateData 배열을 다르게 표현하면 Object.freeze(["<p>", " has sent you a bonk.</p>"]와 같습니다.

(실제로는 templateData에 속성이 하나 더 존재합니다. 이번 글에서는 해당 속성을 사용하지 않지만 정보를 빠짐 없이 제공하기 위해 일러둡니다. 바로 templateData.raw 속성입니다. 이 속성도 태그된 템플릿의 모든 문자열 파트를 요소로 갖는 배열입니다. 하지만 이 배열 속의 문자열들은 우리가 소스 코드에서 보는 것처럼 이스케이프되어 있습니다. 즉 실제 줄바꿈 문자 대신 \n로 표현된 문자열을 담고 있습니다. 표준 태그인 String.raw가 이 raw 문자열을 이용합니다.)

SaferHTML 태그는 함수를 통해 문자열과 대입문을 무한히 다양하게 해석하는 수단으로 활용할 수 있습니다.

아마도 SaferHTML의 구현 방법이 궁금할 것입니다. 한번 구현해보세요. 이것은 정말로 그냥 함수일뿐입니다. Firefox의 개발자 콘솔에서 당신의 코드를 테스트해 보세요.

여기 한가지 가능한 답안이 있습니다 (Gist로 코드를 제공합니다).

function SaferHTML(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);

    // 대입문의 특수 문자들을 이스케이프시켜 표현합니다.
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 템플릿의 특수 문자들은 이스케이프시키지 않습니다.
    s += templateData[i];
  }
  return s;
}

이렇게 태그된 템플릿을 정의하면 SaferHTML`<p>${bonk.sender} has sent you a bonk.</p>`"<p>ES6&lt;3er has sent you a bonk.</p>" 처럼 전개될 것입니다. 이제 당신의 코드는 Hacker Steve <script>alert('xss');</script>처럼 위험한 이름을 갖는 사용자도 안전하게 처리할 수 있습니다.

(그건 그렇고, 함수 안에서 arguments 객체를 다루는 방식이 약간 이상해보였다면 다음주를 기다려주세요. ES6의 새로운 요소입니다. 따로 설명하겠습니다. 아마 좋아하실 겁니다.)

단지 1개의 예제로는 태그된 템플릿(tagged template)의 유연성을 충분히 느낄 수 없었을 것입니다. 앞서 나열했던 템플릿 문자열의 한계점들을 다시 살펴봅시다. 우리가 어떤 일을 더 할 수 있을지 생각해봅시다.

  • 템플릿 문자열은 특수 문자들을 자동으로 이스케이프시켜 표현해주지 않습니다. 하지만 우리가 본 것처럼 태그된 템플릿(tagged template)을 이용하면 당신은 이문제를 직접 해결할 수 있습니다.
    사실 당신은 예제로 제시된 코드보다 훨씬 더 나은 코드를 만들 수 있습니다.

    보안(security) 관점에서, 제가 만든 SaferHTML 함수는 빈약합니다. HTML에는 다양한 위치마다 다양한 방식으로 이스케이프시켜 표현해야 하는 다양한 특수 문자들이 존재합니다. SaferHTML은 그런 모든 경우들을 처리하지 않습니다. 하지만 조금 더 노력하면 SaferHTML 함수를 더 영리하게 만들 수 있습니다. templateData 배열 속의 HTML 문자열을 능동적으로 파싱(parsing)해서 어떤 이스케이프 방법을 사용해야 하는지 알아내는 것입니다. 만약 엘리먼트 속성에 사용되는 문자열이라면 ' 문자와 " 문자를 이스케이프시켜 표현해야 할 것입니다. URL 쿼리에 사용되는 문자열이라면 URL-escaping이 HTML-escaping보다 적절할 것입니다. 이와 같이 각 대입문의 상황에 맞게 이스케이프 방법을 선택할 수 있을 것입니다.

    HTML 파싱(parsing)이 느리기 때문에 현실성 없다고 생각되나요? 다행히도 태그된 템플릿의 문자열 파트는 템플릿을 다시 계산하는 상황에서도 바뀌지 않습니다. SaferHTML은 모든 파싱 결과를 캐시(cache)할 수 있습니다. 그래서 이후 호출될 때 빠르게 결과를 반환합니다. (이 캐시는 WeakMap 객체입니다. 또다른 ES6 요소이며 나중에 다른 글에서 다룰 것입니다.)

  • 템플릿 문자열에는 내장된 다국어 지원 기능이 없습니다. 하지만 태그를 이용하면 그런 기능을 추가할 수 있습니다. Jack Hsu의 블로그 글이 태그된 템플릿(tagged template)을 이용해서 다국어를 지원하는 방법을 설명하고 있습니다. 맛보기 삼아 간단히 예를 들자면 다음과 같이 다국어를 지원할 수 있습니다.

    i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
    // => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.
    

    이 예에서 nameamount는 JavaScript 코드입니다. 하지만 코드 중에 이상하고 낯선 표현이 보입니다. :c(CAD) 입니다. 이것은 Jack이 템플릿의 문자열 파트에 끼워넣은 것입니다. JavaScript 코드는 당연히 JavaScript 엔진에 의해 처리됩니다. 문자열 파트는 Jack의 i18n 태그에 의해 처리됩니다. i18n 문서를 보면 :c(CAD) 표시가 캐나다 달러를 나타낸다는 것을 알 수 있습니다. 이 코드는 결과적으로 amount를 캐나다 달러로 표시합니다.

    이것이 태그된 템플릿(tagged template)입니다.

  • 템플릿 문자열은 Mustache나 Nunjucks의 대체품이 아닙니다. 템플릿 문자열에는 루푸문이나 조건문을 위한 문법이 없습니다. 이제 우리는 이 제약을 어떻게 극복할지 알아볼 것입니다. 그렇죠? 만약 JS가 이 기능을 제공하지 않는다면 이 기능을 제공하는 태그를 작성하면 됩니다.

    // ES6 태그된 템플릿(tagged templates)을 기반으로 만든
    // 순전히 샘플로 만들어본 템플릿 언어
    var libraryHtml = hashTemplate`
      <ul>
        #for book in ${myBooks}
          <li><i>#{book.title}</i> by #{book.author}</li>
        #end
      </ul>
    `;
    

유연성은 거기서 그치지 않습니다. 한가지 이러둘 것이 있습니다. 태그 함수의 인자는 문자열로 자동 변환되지 않습니다. 태그 함수의 인자로는 어떤 것이 와도 좋습니다. 리턴 값도 마찬가지입니다. 태그된 템플릿(tagged template) 자체가 문자열일 필요도 없습니다! 당신은 정규식(regular expression), DOM 트리, 이미지, 비동기 작업 전체를 대표하는 프라미스(promise), JS 데이터 구조체, GL 쉐이더(shader)… 등 어떤 것을 만들 때도 태그를 이용할 수 있습니다.

태그된 템플릿(tagged template)은 특정 분야를 위한 강력한 랭귀지를 만드는 일에 라이브러리 설계자들을 초대합니다. 이렇게 만든 랭귀지들은 전혀 JS처럼 보이지 않을 것입니다. 하지만 여전히 JS를 매끈하게 내장하고 있으며 JS의 다른 기능들과 지능적으로 연동됩니다. 거칠게 말해서, 저는 이런 개념을 다른 어떤 랭귀지에서도 본 적이 없습니다. 태그된 템플릿(tagged template)이 우리를 어디까지 데려갈지 모르겠습니다. 아주 흥미로운 가능성입니다.

언제부터 쓸 수 있나요?

서버에서는 io.js에서 ES6 템플릿 문자열을 지금 당장 쓸 수 있습니다.

브라우저에서는 Firefox 34+ 버전이 템플릿 문자열을 지원합니다. Firefox의 템플릿 문자열 기능은 지난 여름 Guptha Rajagopal이 인턴 프로젝트를 하면서 구현했습니다. Chrome 41+ 버전도 템플릿 문자열을 지원합니다. 하지만 IE와 Safari는 지원하지 않습니다. 만약 웹 상에서 템플릿 문자열을 쓰고 싶다면 현재로서는 Babel이나 Traceur를 사용하는 것이 좋을 것 같습니다. TypeScript에서도 템플릿 문자열을 지금 당장 사용할 수 있습니다!

잠깐요-마크다운은 어떻게 되나요?

흠?

아. …좋은 질문입니다.

(지금부터 하는 얘기는 JavaScript와 관련 없습니다. 만약 마크다운을 쓰고 있지 않다면, 건너뛰어도 좋습니다.)

템플릿 문자열이 도입되면서, 이제 마크다운과 JavaScript가 모두 ` 문자를 특별한 의미로 사용하게 되었습니다. 마크다운에서 ` 문자는 본문 중의 code 영역을 표시하는 구분자로 사용됩니다.

이 때문에 사소한 문제가 발생합니다! 마크다운 문서를 다음과 같이 작성한다고 가정해 봅시다.

To display a message, write `alert(`hello world!`)`.

이 마크다운은 다음과 같이 보여질 것입니다.

To display a message, write alert(hello world!).

출력된 결과에 백틱 문자가 존재하지 않는 것을 눈여겨 보세요. 마크다운은 4개의 백틱 모두를 코드 구분자로 해석해서 모두 HTML 태그로 대치합니다.

이 문제는 마크다운이 지원하는 트릭을 이용하면 피할 수 있습니다. 다음처럼 코드 영역을 구분하기 위해 백틱을 여러개 쓰면 됩니다.

To display a message, write ``alert(`hello world!`)``.

여기 있는 Gist가 관련 내용을 자세히 설명합니다. 이 Gist 자체가 마크다운으로 쓰여져 있습니다. 소스 코드도 확인해 보세요.

다음 시간 예고

다음 주에, 우리는 프로그래머들이 다른 랭귀지에서 10년 넘게 즐겨오던 2가지 기능을 살펴볼 것입니다. 하나는 가능하다면 인자를 쓰고 싶어하지 않는 사람들을 위한 것입니다. 그리고 다른 하나는 가능하다면 많은 인자를 쓰고 싶어하는 사람들을 위한 것입니다. 물론 함수 인자에 대해 이야기 하는 것입니다. 이 2가지 기능은 정말 우리 모두를 위한 것입니다.

이 2가지 기능을 Firefox에 이 기능을 직접 구현한 사람의 관점에서 살펴볼 것입니다. 그러니 다음 주에 우리와 함께 해주세요. 게스트 저자 Benjamin Peterson이 ES6의 디폴트 파라메터(default parameter)와 레스트 파라메터(rest parameter)에 대해 자세히 설명해줄 것입니다.

이 글은 이 쓴 ES6 In Depth: Template strings의 한국어 번역본입니다.

작성자: ingeeKim

"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.

ingeeKim가 작성한 문서들…


댓글이 없습니다.

댓글 쓰기