템플릿 리터럴은 단순히 문자열 보간(interpolation)만을 위한 문법이 아닙니다. Tagged Template을 사용하면 문자열 처리를 완전히 커스터마이징할 수 있고, styled-components 같은 라이브러리의 핵심 원리이기도 합니다.

템플릿 리터럴 기본

JS
// 기본 문자열 보간
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 이 됩니다. 문자열 조각과 값을 분리해서 받습니다.

JS
function tag(strings, ...values) {
  console.log(strings); // ["안녕 ", "님, ", "세입니다."]
  console.log(values);  // ["정훈", 25]
}

const name = "정훈";
const age = 25;
tag`안녕 ${name}님, ${age}세입니다.`;

strings는 항상 values보다 하나 더 많습니다.

기본 결합 구현

JS
function defaultTag(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ?? "");
  }, "");
}

// 일반 템플릿 리터럴과 동일한 결과
console.log(defaultTag`Hello ${name}!`); // "Hello 정훈!"

실전 활용 1: HTML 이스케이프

XSS 공격을 방지하기 위해 사용자 입력을 이스케이프하는 패턴입니다.

JS
function safeHtml(strings, ...values) {
  const escape = (str) =>
    String(str)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");

  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>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</div>

실전 활용 2: 하이라이트

JS
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 원리)

JS
// 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 쿼리 빌더

JS
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)

JS
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를 사용하면 이스케이프 시퀀스가 처리되지 않은 원본 문자열을 얻을 수 있습니다.

JS
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"

중첩 템플릿

JS
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>

주의사항

JS
// 태그 함수에서 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을 사용하고 있는 것입니다.

댓글 로딩 중...