자바스크립트/모던 자바스크립트

모던 자바스크립트, 템플릿 리터럴과 템플릿 태그 함수

Jake Seo 2023. 3. 8. 23:06

템플릿 리터럴

  • 정규표현식 리터럴(//g), 문자열 리터럴(""), 배열 리터럴([]) 처럼 리터럴의 한 종류이다.
  • 백틱( ` )으로 구분되는 리터럴이며, 악센트(accent)라고도 한다.
  • 리터럴 중간에 Place Holder (${}) 를 이용하여 표현식을 리터럴에 삽입할 수 있다.
    • 문자열을 이용할 때 "myName = " + name 과 같은 행위를 편하게 만들어준다.
  • 이스케이프 시퀀스는 일반 문자열과 동일하게 동작한다.
    • \n 가 중간에 온다면, 동일하게 개행으로 취급되는 것을 볼 수 있다.

리터럴이란, 더이상 쪼갤 수 없는 값의 표현이다.
심볼(Symbol()) 도 리터럴이다.

기본 사용법

const name = "jake";
console.log(`my name is ${name}`);

const employees = ["jack", "paul", "haul"];
console.log(`our company has ${employees.join(", ")}`);

재사용 팁

const name = "jake";
const age = "20";
const height = "193cm";

console.log(`name: ${name}, age: ${age}, height: ${height}`);
  • console.log() 를 이용해 한번 출력해버리면 템플릿이 사라진다.
const introduce = ({ name, age, height }) =>
  `name: ${name}, age: ${age}, height: ${height}`;
  • 초보적인 내용이지만, 이렇게 함수로 래핑하면 자연스레 재사용이 가능해진다.

템플릿 태그 함수 (Tagged templates)

사실상 템플릿 리터럴이야 30초면 이해하고 코드에 적용하지만, 태그 함수의 경우는 아마 적용해서 쓰고 있는 사람이 많진 않을 것이라 생각한다.

  • 템플릿 리터럴을 이용해 함수를 호출하는 방식을 말한다.
  • 문자열을 조작하거나 포맷팅할 때 특히 유용하다.
  • 반복하여 문자열 조작 작업을 할 때 태그 함수를 통해 캡슐화가 가능하다.
    • 코드가 더 모듈화되고 재사용성이 뛰어난 형태로 변한다.

기본 사용법

const a = 1;
const b = 2;
const c = 3;

const templateTag = (template, ...args) => {
  console.log("template", template);
  console.log("args", args);
};

templateTag`a = ${a}, b = ${b}, c = ${c}`;

// template (4) ['a = ', ', b = ', ', c = ', '', raw: Array(4)]
// args (3) [1, 2, 3]
  • 함수에 인자를 넘길 때, 템플릿 문자열 부분과 치환자 부분이 순서대로 넘어온다.
  • ...values 를 통해 치환자 부분을 받은 이유는 치환자 몇 개가 넘어올지 확신할 수 없기 때문이다.
  • template 부에 raw 라고 하는 배열이 있는데, 이는 이스케이프 전의 문자열을 갖고 있다.
    • 만일 \n 을 넣었다면, raw 에는 개행이 아니라 문자열 그대로의 \n 가 담겨 있다.

raw 를 사용하는 예제

const example = (template) => {
  console.log("template[0]", template[0]);
  console.log("template.raw[0]", template.raw[0]);
};

example`\u000A\x0a\n`;

/*
template[0] 


*/

/*
template.raw[0] \u000A\x0a\n
*/
  • template[0] 에서는 \u000A\x0a\n 이 전부 개행으로 변환되어서 개행만 보인다.
  • template.raw[0] 에선 개행으로 변환되지 않은 날것의 문자열을 보여주고 있다.
const example2 = (template) => {
  console.log("template", template);
  console.log("template.raw", template.raw);
};

example2`Has invalid escape: \ufoo${","}Has only valid escapes:\n`;

/*
template (2) [undefined, 'Has only valid escapes:\n', raw: Array(2)]
template.raw (2) ['Has invalid escape: \\ufoo', 'Has only valid escapes:\\n']
*/
  • 유효하지 않은 이스케이프 시퀀스는 undefined 로 치환된다.
  • \ufoo 는 유효한 유니코드 문자가 아니다.
    • 코드 포인트 \u0041 (65)A 로 표현된다

raw 는 이스케이프가 일어나지 않는다는 점을 활용하여 정규표현식을 작성하기에도 좋다.
정규표현식 리터럴이 따로 존재하는 이유는 이스케이프 등 일반 문자열 리터럴에서는 예외가 너무 많기 때문이다.

raw 로 정규표현식 객체를 만드는 예제

const createRegex = (template, ...values) => {
  const source = template.raw.reduce(
    (acc, str, index) => acc + values[index - 1] + str
  );

  const match = /^\/(.+)\/([a-z]*)$/.exec(source);

  if (!match) {
    throw new Error("Invalid regular expression");
  }

  const [, expr, flags = ""] = match;
  return new RegExp(expr, flags);
};

const escapeRegExp = (s) => String(s).replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
const alternatives = ["this", "that", "the other"];
const rex = createRegex`/\b(?:${alternatives.map(escapeRegExp).join("|")})\b/i`;

console.log(
  `alternatives.map(escapeRegExp).join("|")`,
  alternatives.map(escapeRegExp).join("|")
);

console.log("rex", rex);

const test = (str, expect) => {
  const result = rex.test(str);
  console.log(`str: ${result}, ${result === expect ? "GOOD" : "BAD"}`);
};

test("doesn't have either", false);
test("has_this_but_not_delimited", false);
test("has this", true);
test("has the other ", true);
  • 문자가 포함되어 있는지 확인하는 간단한 정규표현식 예제이다.

간단 DSL 예제

const html = (strings, ...values) => {
  const joined = strings.reduce((result, string, i) => {
    result += string;

    if (values[i]) {
      result += values[i];
    }

    return result;
  }, "");

  // Parse the joined string as an HTML element
  const parser = new DOMParser();
  const parsed = parser.parseFromString(joined, "text/html");
  return parsed.body.firstChild;
};

const message = "Hello World!";

const elem = html`
  <div class="container">
    <h1>${message}</h1>
    <p>This is a paragraph.</p>
  </div>
`;

console.log(elem);
document.body.appendChild(element);
  • 얼핏 보면, 'element.innerHTML = xxx 와 무슨 차이가 있나?' 와 같은 생각을 하기 쉽다.
  • 태그 중간중간에 나오는 placeholder 에 해당하는 값을 하나씩 검증하거나 필터링하거나 매핑할 때 매우 유리하다.

간단 DSL 예제 발전시키기

const insertHTML =
  (selector, { position = "beforeend", prefix = "", suffix = "" }) =>
  (strings, ...values) => {
    const $selected = document.querySelector(selector);

    if ($selected) {
      const htmlString = strings.reduce((result, string, i) => {
        result += string;

        if (values[i]) {
          result += prefix + values[i] + suffix;
        }

        return result;
      }, "");

      $selected.insertAdjacentHTML(position, htmlString);

      return;
    }

    console.warn(
      `selector: ${selector} 에 해당하는 엘리먼트가 존재하지 않습니다.`
    );

    return;
  };

const content = "myButton";

insertHTML("#dpHeader", { prefix: "(", suffix: ")" })`
  <button>${content}</button>
`;
  • selector 를 통해 엘리먼트를 선택하고 해당 엘리먼트에 DOM Element 를 삽입한다.
  • DOM 엘리먼트에 삽입되는 Place holder 에 대해 prefixsuffix 도 주었다.
반응형