
태그된 템플릿 리터럴은 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.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다. Trie는 무…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
느린 네트워크에서 큰 이미지를 열면 가끔 화면이 위에서 아래로 채워진다. 마치 누군가 아주 성실하게 롤러로 이미지를 칠하는 것처럼 보인다. 물론 브라우저 안에 그런 직원은 없다. 있다면 우리보다 야근을 더 많이 하고 있을 것이다. 이 현상은 단순한 시각 효과가 아니라 네트워크 전송, 브…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
다운로드 화면에서 진행 막대가 조금씩 차오르면 이상하게 안심된다. 반대로 스피너만 계속 돌면 파일이 오는 중인지, 서버가 고민 중인지, 내 인생이 잠깐 멈춘 건지 알 수 없다. 사용자는 둘 다 “다운로드 중”이라고 느끼지만, 내부적으로는 꽤 다른 상황일 수 있다. 진행률 계산 자체는 복…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감이 온다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 보고 있는데, Java의 InputStream, Go의 io.Reader, Rust의 Read와 Write가 멀리서…
2026.06.01
React Server Components를 위한 컴포넌트 아키텍처
이 포스트는 Vercel의 Next.js 팀 소속 개발자 Aurora Scharff가 자신의 블로그에 올린 Component Architecture for React Server Components 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
댓글
댓글을 불러오는 중...