Tagged Template Literal

작성일:2024.11.15|조회수:0

Tagged Template Literal

태그된 템플릿 리터럴은 ES6부터 도입된 자바스크립트 기능이다. 이 문법은 템플릿 리터럴을 단순한 문자열 보간 도구로 사용하는 데서 한 단계 더 나아가, 템플릿 자체를 함수로 전달하여 가공할 수 있게 한다는 점에서 의미가 크다. 즉, 문자열을 만드는 과정에 개발자가 직접 개입할 수 있는 훅(hook)을 제공하는 셈이다.

일반적인 템플릿 리터럴은 문자열을 표현하는 데 그 목적이 한정된다. ${} 내부의 표현식은 평가되어 문자열로 치환되고, 그 결과가 그대로 이어 붙여진다. 하지만 태그된 템플릿 리터럴에서는 이 과정이 자동으로 끝나지 않는다. 템플릿 리터럴 바로 앞에 위치한 태그 함수가 문자열 조합의 주도권을 가져가며, 문자열의 각 조각과 삽입된 값들을 분리된 상태로 전달받아 원하는 방식으로 처리할 수 있다.

형태만 놓고 보면 태그 함수는 일반 함수와 크게 다르지 않다. 다만 호출 방식이 독특하다. 태그 함수는 다음과 같은 시그니처를 가진다.

TS
function myTag(
  strings: TemplateStringsArray,
  ...values: unknown[]
): string {
  // ...
}

첫 번째 인자로 전달되는 strings는 템플릿 리터럴에서 고정된 문자열 조각들만 모아둔 배열이다. 그리고 ${} 안에 들어간 표현식들의 결과값은 나머지 인자(...values)로 순서대로 전달된다. 템플릿에 어떤 값이 들어올지 알 수 없기 때문에 unknown으로 선언했지만, 상황에 따라 string[], number[], 혹은 제네릭을 활용해 더 엄격한 타입으로 제한할 수도 있다.

프론트엔드 개발자라면 이 문법을 처음 보는 순간 떠오르는 대표적인 사례가 있다. 바로 styled-components의 styled 함수다. 이 함수는 태그된 템플릿 리터럴을 통해 CSS 문자열과 동적 값을 받아 React 컴포넌트를 생성한다. 또 다른 예로는 Apollo Client나 graphql-request에서 제공하는 gql 함수가 있다. 이 역시 태그된 템플릿 리터럴을 통해 GraphQL 쿼리를 파싱하고, 내부적으로 의미 있는 구조로 변환한다.

이 문법을 이해하는 데 있어 꼭 기억해두어야 할 중요한 규칙이 하나 있다. strings의 길이는 언제나 values보다 정확히 하나 더 길다는 점이다. 이는 템플릿 리터럴이 ${}를 기준으로 문자열을 나누기 때문이다. 분리 기준이 되는 토큰의 개수보다 항상 하나 더 많은 조각이 생긴다고 생각하면 이해하기 쉽다.

SH
"가나다나가".split("나"); // ["가", "다", "가"]

const 나 = "나";
myTag`가${나}다${나}가`;
// strings: ["가", "다", "가"]
// values: ["나", "나"]

이 구조 덕분에 태그 함수는 문자열을 단순히 이어 붙이는 것이 아니라, 각 값이 어떤 문맥에서 사용되었는지를 판단할 수 있다. 바로 이 지점이 태그된 템플릿 리터럴의 핵심적인 활용 포인트다.

그렇다면 이 문법은 실제로 어떤 상황에서 유용할까? 가장 먼저 떠올릴 수 있는 활용은 값 검증과 의미 부여다. ${}로 전달된 값들을 단순히 문자열로 변환하지 않고, 그 관계를 검사하거나 특정 규칙을 강제할 수 있다. 또한 객체가 그대로 문자열로 변환되어 [object Object]가 되는 문제를 피하고, 의도한 형태로 출력하도록 제어할 수도 있다.

아래 예시는 성적 정보를 서술하는 문장을 태그된 템플릿 리터럴로 처리하면서, 전달된 숫자들 간의 관계를 검증하는 예시다.

TS
function score(
  strings: TemplateStringsArray,
  name: string,
  total: number,
  korean: number,
  english: number,
  math: number
) {
  if (korean + english + math !== total) {
    throw new Error(
      "세 과목 점수의 합이 총점과 일치하지 않습니다."
    );
  }

  return strings.reduce((acc, str, i) => {
    const value = [name, total, korean, english, math][i];
    return acc + str + (value ?? "");
  }, "");
}

이 함수는 단순히 문자열을 생성하는 역할을 넘어, 도메인 규칙을 강제하는 진입점이 된다. 사용 예시는 다음과 같다.

JS
const name = "철수";
const totalPoint = 270;
const koreanPoint = 90;
const englishPoint = 90;
const mathPoint = 90;

score`학생 ${name}이 시험에서 받은 점수의 총 합은 ${totalPoint}점이며,
국어 ${koreanPoint}점, 영어 ${englishPoint}점, 수학 ${mathPoint}점을 획득했습니다.`;

이제 이 문장은 단순한 문자열이 아니다. 템플릿의 형태 자체가 하나의 인터페이스가 되고, 태그 함수는 그 인터페이스를 해석하는 로직이 된다. 값의 개수나 의미가 맞지 않으면 즉시 에러를 발생시킬 수 있고, 이는 런타임에서 조용히 잘못된 문자열을 만들어내는 것보다 훨씬 안전하다.

더 나아가면 이 패턴은 국제화(i18n), 로깅 포맷팅, 보안 처리(예: SQL injection 방지), 도메인 특화 언어(DSL) 구현 등 다양한 영역으로 확장할 수 있다. 중요한 점은 태그된 템플릿 리터럴이 “문자열을 예쁘게 만드는 문법”이 아니라, 문자열이 생성되는 순간에 의미와 제약을 부여할 수 있는 도구라는 사실이다.

태그된 템플릿 리터럴은 자바스크립트 문법 중에서도 상대적으로 덜 사용되지만, 한 번 그 구조를 이해하고 나면 단순한 문자열 처리 이상의 가능성을 가진다는 것을 느끼게 된다. 값과 문맥을 동시에 다룰 수 있다는 점에서, 이 문법은 라이브러리 설계나 추상화 계층에서 특히 강력한 무기가 된다.

더 읽어보기

  • 2026.03.13

    Streams API 3. 바이트 스트림과 실전 파이프라인

    앞선 두 글에서 우리는 Web Streams API의 표면과 의미론을 정리했다. 이제 남은 질문은 하나이다. 이 지식을 실제 어디에 써먹을 것인가. 이 질문에 답하는 순간 스트림 학습은 비로소 완성된다. 표준을 읽고 메서드를 아는 것만으로는 충분하지 않다. fetch() 응답 본문을 어…

  • 2026.03.13

    Streams API 2. 상태와 백프레셔의 의미론

    스트림을 처음 배울 때는 읽고 쓰는 예제가 꽤 단순해 보인다. getReader()로 읽고, getWriter()로 쓰고, pipeTo()로 연결하면 끝나는 것처럼 느껴진다. 실제로 짧은 데모는 이 정도만 알아도 돌아간다. 그러나 실무에서 스트림 코드를 망가뜨리는 원인은 거의 언제나 더…

  • 2026.03.13

    Streams API 1. 읽기와 쓰기의 출발점

    자바스크립트에서 비동기를 배울 때 우리는 대개 Promise와 async, await부터 익힌다. 이 조합은 한 번의 결과를 기다리는 문제에는 매우 강력하다. 그러나 데이터가 한 번에 끝나지 않고 계속 흘러들어오는 상황에서는 이야기가 달라진다. 네트워크 응답이 길게 이어지거나, 큰 파일…

  • 2026.04.11

    Trie 자료구조

    문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…

  • 2026.03.19

    Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까

    웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…

댓글

댓글을 불러오는 중...