템플릿 리터럴 심화 — Tagged Template으로 DSL 만들기
템플릿 리터럴은 단순히 문자열 보간(interpolation)만을 위한 문법이 아닙니다. Tagged Template을 사용하면 문자열 처리를 완전히 커스터마이징할 수 있고, styled-components 같은 라이브러리의 핵심 원리이기도 합니다.
템플릿 리터럴 기본
// 기본 문자열 보간
const name = "정훈";
const greeting = `안녕하세요, ${name}님!`;
// 표현식 삽입
const price = 10000;
const tax = 0.1;
console.log(`세후 가격: ${price * (1 + tax)}원`); // "세후 가격: 11000원"
// 여러 줄 문자열
const html = `
<div>
<h1>${name}</h1>
<p>개발자</p>
</div>
`;
Tagged Template 함수
백틱 앞에 함수를 붙이면 Tagged Template 이 됩니다. 문자열 조각과 값을 분리해서 받습니다.
function tag(strings, ...values) {
console.log(strings); // ["안녕 ", "님, ", "세입니다."]
console.log(values); // ["정훈", 25]
}
const name = "정훈";
const age = 25;
tag`안녕 ${name}님, ${age}세입니다.`;
strings는 항상 values보다 하나 더 많습니다.
기본 결합 구현
function defaultTag(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] ?? "");
}, "");
}
// 일반 템플릿 리터럴과 동일한 결과
console.log(defaultTag`Hello ${name}!`); // "Hello 정훈!"
실전 활용 1: HTML 이스케이프
XSS 공격을 방지하기 위해 사용자 입력을 이스케이프하는 패턴입니다.
function safeHtml(strings, ...values) {
const escape = (str) =>
String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
return strings.reduce((result, str, i) => {
return result + str + (i < values.length ? escape(values[i]) : "");
}, "");
}
const userInput = '<script>alert("xss")</script>';
const html = safeHtml`<div>${userInput}</div>`;
console.log(html);
// <div><script>alert("xss")</script></div>
실전 활용 2: 하이라이트
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = i < values.length ? `<mark>${values[i]}</mark>` : "";
return result + str + value;
}, "");
}
const keyword = "JavaScript";
const output = highlight`${keyword}는 프론트엔드의 핵심 언어입니다.`;
console.log(output);
// <mark>JavaScript</mark>는 프론트엔드의 핵심 언어입니다.
실전 활용 3: CSS-in-JS (styled-components 원리)
// styled-components의 핵심 원리를 단순화한 예제
function css(strings, ...values) {
return (props) => {
return strings.reduce((result, str, i) => {
const value = typeof values[i] === "function"
? values[i](props)
: values[i] ?? "";
return result + str + value;
}, "");
};
}
// 사용 예시
const buttonStyle = css`
background: ${(props) => (props.primary ? "blue" : "gray")};
color: white;
padding: ${(props) => (props.large ? "16px" : "8px")};
`;
console.log(buttonStyle({ primary: true, large: false }));
// background: blue; color: white; padding: 8px;
실전 활용 4: SQL 쿼리 빌더
function sql(strings, ...values) {
const params = [];
const query = strings.reduce((result, str, i) => {
if (i < values.length) {
params.push(values[i]);
return result + str + `$${params.length}`; // 파라미터 플레이스홀더
}
return result + str;
}, "");
return { query, params };
}
const name = "정훈";
const age = 25;
const result = sql`SELECT * FROM users WHERE name = ${name} AND age > ${age}`;
console.log(result.query); // "SELECT * FROM users WHERE name = $1 AND age > $2"
console.log(result.params); // ["정훈", 25]
이 패턴을 사용하면 SQL 인젝션을 방지할 수 있습니다.
실전 활용 5: 국제화(i18n)
function i18n(strings, ...values) {
// 실제로는 번역 데이터베이스에서 조회
const translations = {
"안녕하세요, %님! %개의 알림이 있습니다.":
"Hello, %! You have % notifications.",
};
const template = strings.join("%");
const translated = translations[template] || template;
let result = translated;
values.forEach((value) => {
result = result.replace("%", value);
});
return result;
}
const name = "정훈";
const count = 5;
console.log(i18n`안녕하세요, ${name}님! ${count}개의 알림이 있습니다.`);
// "Hello, 정훈! You have 5 notifications."
strings.raw
Tagged Template에서 strings.raw를 사용하면 이스케이프 시퀀스가 처리되지 않은 원본 문자열을 얻을 수 있습니다.
function showRaw(strings) {
console.log(strings[0]); // 줄바꿈 문자 (처리됨)
console.log(strings.raw[0]); // "\\n" (원본 그대로)
}
showRaw`Hello\nWorld`;
// String.raw — 내장 태그 함수
console.log(String.raw`Hello\nWorld`); // "Hello\nWorld" (줄바꿈 안 됨)
// 파일 경로에서 유용
const path = String.raw`C:\Users\정훈\Documents`;
console.log(path); // "C:\Users\정훈\Documents"
중첩 템플릿
const items = ["사과", "바나나", "체리"];
// 템플릿 안에 템플릿
const html = `
<ul>
${items.map((item) => `<li>${item}</li>`).join("\n ")}
</ul>
`;
console.log(html);
// <ul>
// <li>사과</li>
// <li>바나나</li>
// <li>체리</li>
// </ul>
주의사항
// 태그 함수에서 undefined를 반환하면 "undefined" 문자열이 됨
function broken(strings) {
// return 없음
}
console.log(broken`hello`); // undefined
// 템플릿 리터럴은 즉시 평가됨
const name = "정훈";
const greeting = `Hello, ${name}`;
// name을 나중에 바꿔도 greeting은 변하지 않음
**기억하기 **: Tagged Template은 문자열 조각(strings)과 삽입된 값(values)을 분리해서 받는 함수입니다. 이 구조를 이용하면 HTML 이스케이프, SQL 파라미터화, CSS-in-JS 등 다양한 DSL을 만들 수 있습니다. styled-components를 쓴다면, 이미 Tagged Template을 사용하고 있는 것입니다.
댓글 로딩 중...