
태그된 템플릿 리터럴은 ES6부터 도입된 자바스크립트 기능이다. 이 문법은 템플릿 리터럴을 단순한 문자열 보간 도구로 사용하는 데서 한 단계 더 나아가, 템플릿 자체를 함수로 전달하여 가공할 수 있게 한다는 점에서 의미가 크다. 즉, 문자열을 만드는 과정에 개발자가 직접 개입할 수 있는 훅(hook)을 제공하는 셈이다.
일반적인 템플릿 리터럴은 문자열을 표현하는 데 그 목적이 한정된다. ${} 내부의 표현식은 평가되어 문자열로 치환되고, 그 결과가 그대로 이어 붙여진다. 하지만 태그된 템플릿 리터럴에서는 이 과정이 자동으로 끝나지 않는다. 템플릿 리터럴 바로 앞에 위치한 태그 함수가 문자열 조합의 주도권을 가져가며, 문자열의 각 조각과 삽입된 값들을 분리된 상태로 전달받아 원하는 방식으로 처리할 수 있다.
형태만 놓고 보면 태그 함수는 일반 함수와 크게 다르지 않다. 다만 호출 방식이 독특하다. 태그 함수는 다음과 같은 시그니처를 가진다.
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보다 정확히 하나 더 길다는 점이다. 이는 템플릿 리터럴이 ${}를 기준으로 문자열을 나누기 때문이다. 분리 기준이 되는 토큰의 개수보다 항상 하나 더 많은 조각이 생긴다고 생각하면 이해하기 쉽다.
"가나다나가".split("나"); // ["가", "다", "가"]
const 나 = "나";
myTag`가${나}다${나}가`;
// strings: ["가", "다", "가"]
// values: ["나", "나"]이 구조 덕분에 태그 함수는 문자열을 단순히 이어 붙이는 것이 아니라, 각 값이 어떤 문맥에서 사용되었는지를 판단할 수 있다. 바로 이 지점이 태그된 템플릿 리터럴의 핵심적인 활용 포인트다.
그렇다면 이 문법은 실제로 어떤 상황에서 유용할까? 가장 먼저 떠올릴 수 있는 활용은 값 검증과 의미 부여다. ${}로 전달된 값들을 단순히 문자열로 변환하지 않고, 그 관계를 검사하거나 특정 규칙을 강제할 수 있다. 또한 객체가 그대로 문자열로 변환되어 [object Object]가 되는 문제를 피하고, 의도한 형태로 출력하도록 제어할 수도 있다.
아래 예시는 성적 정보를 서술하는 문장을 태그된 템플릿 리터럴로 처리하면서, 전달된 숫자들 간의 관계를 검증하는 예시다.
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 ?? "");
}, "");
}이 함수는 단순히 문자열을 생성하는 역할을 넘어, 도메인 규칙을 강제하는 진입점이 된다. 사용 예시는 다음과 같다.
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.03.04
JavaScript를 위한 더 나은 Streams API가 필요하다
이 포스트는 node.js의 코어 컨트리뷰트이며 Cloudflare Workers 팀 소속 개발자 James M Snell이 cloudflare 블로그에 올린 We deserve a better streams API for JavaScript 게시글을 번역한 것이다. 번역하는 과정에서…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
댓글
댓글을 불러오는 중...