Handlebars

yunazzi·2024년 6월 17일

01. Handlebars.js 란?

: Mustache를 기반으로 구현한 간단한 templating language 이다.

핸들바 표현식은 {{ some contents }} 이다.
템플릿이 실행되면 이러한 표현식은 입력개체의 값으로 대체된다.
표현식에 사용되는 중괄호 {}가 콧수염(Mustache)을 닮았다고 하여 콧수염괄호라고도 한다.

장단점

장점
• Java, Django, Ruby, PHP, Scala 에서 사용 할 수 있다.
• 문법이 깔끔하다.
• Logic 과 Markup 을 깔끔하게 분리 할 수 있다. {{ }} 안에 있는 것이 Logic 이다.
• partials 를 지원한다.
• 컴파일 단계에서 코드가 해석이 되므로, 클라이언트에서 따로 코드를 해석하는 단계가 필요없다. 그러므로 로딩이 빠르다.
• HTML 오픈 소스를 복사/붙여넣기 가 가능하다. (pug는 불가능, pug 형식에 맞게 다시 작성해야 함)

단점
• auto-complete, syntax highlighting 등 과 같은 기능을 에디터에서 제공해 주지 않는다.
• 크게 단점이라고 느껴지는 점이 없어 보인다.

02. 기본 사용방법

표현식

{{title}} 과 같은 형태로 표현된다.
-- 현재 문맥에서 title이라는 속성을 찾아서 대체하는 의미

{{section.title}} 과 같은 형태로 dot(.)로 분리된 경로탐색도 가능하다.
--현재 문맥에서 section을 찾고, title속성을 찾아 대체하는 의미

{{section/title}} 과 같이 / 문법을 사용할 수 있다. 식별자는 아래 열거된UniCode를 제외하고 모두 사용가능하다.
-- Whitespace ! " # % & ' ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~

• 배열 접근 표현식을 통해 Property에 접근할 수 있다.

1
2
3
4
5
6
{{#each section.[3].titles}}
    <h1>{{subject}}</h1>
    <div>
      {{body}}
    </div>
  {{/each}}
cs
-- section배열의 3번째 titles의 모든 속성을 문맥으로 갖으면서 subject와 body에 접근한다.

{{! }}블록으로 주석을 추가할 수 있다.
{{log}}블록으로 템플릿 출력시 로깅할 수 있다.
{{{--}}} 핸들바가 값을 이스케이프하지 않길 원할때 사용한다.


Partials

여러페이지에서 똑같은 구성요소를 재사용할때 사용하는 기능
(웹사이트의 header나 footer는 여러페이지에서 동일하게 사용되므로 이를 Partial로 만들어관리하면 효율적이다.)

Basic Partials
partial 사용문법 : Handlebars.registerPartial();

Dynamic Partials
Partial를 동적으로 생성하거나 변경하는 것
{{> (whichPartial)}}

  1. 동적 콘텐츠 로딩
    : 사용자에 따라 또는 특정조건에 따라 다른내용을 보여줘야할때 사용 (ex. 로그인/비로그인 사용자 메뉴)

  2. 템플릿 구성요소의 재사용
    : 공통부분을 Partial로 만들고, 필요한 곳에서 동적으로 불러와 사용 (코드중복 ↓, 유지보수 용이)

  3. 조건부 렌더링
    : 특정조건에 따라 다른 Partial을 로드하거나 렌더링
    (ex. 블로그 포스트의 요약부분을 짧게 보여주다가 더보기버튼을 누르면 전체포스트를 보여주는 기능 구현할때 유용)

Partial Contexts
-
Partial Parameters
사용자 정의데이터는 매개변수를 통해서 partial에 전달될 수 있다.

//①번 예시
//Template
{{> myPartial parameter = favoriteNumber}}
  
//{{>myPartial}}
The result is {{parameter}}

//Input
{favoriteNumber : 123}
  
//Output
The result is 123
//②번 예시
//Template
{{#each people}}
	{{> myPartial prefix=../prefix firstname=firstname lastname=lastname}}.
{{/each}}
  
//{{>myPartial}}
{{prefix}},{{firstname}} {{lastname}}

//Input
{
	people : [
		{
			firstname: "Nills",
  			lastname : "Knappmeier"
		},
		{
            firstname: "Yehuda",
            lastname: "Katz",
    	},
	],
  prefix : "Hello",
}
  
//Output
Hello, Nils Knappmeier.
Hello, Yehuda Katz.

Partial Blocks
동적으로 변경될 수 있는 콘텐츠 블록을 삽입할 수 있도록하는 기능
Partial의 구조는 고정하면서도 내부 콘텐츠는 유연하게 변경할 수 있음

  1. Partial 정의
    : Partial 내부에서 {{> @partial-block }}를 사용하여 콘텐츠 블록을 삽입할 위치를 지정한다.
<div class="layout">
  <header>
    <h1>{{title}}</h1>
  </header>
  <main>
    {{> @partial-block }}
  </main>
  <footer>2024
  </footer>
</div>
  1. Partial 호출
    : Partial을 호출할 때, 블록 콘텐츠를 함께 정의한다.
{{#> layout title="My Page Title"}}
  <p>This is the main content of the page.</p>
{{/layout}}

Inline Partials
템플릿 파일내에서 직접 Partial을 정의하고 사용하는 방법
별도의 파일로 Partial을 분리하는 대신, 하나의 템플릿 파일내에서 Partial을 정의하고, 그자리에서 바로 사용할 수 있게 해준다.

  1. Partial 정의와 사용
    : 템플릿 파일 내에서 {{#*inline "partialName"}}...{{/inline}}문법을 사용하여 Partial을 정의할 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Inline Partials Example</title>
</head>
<body>
    {{#*inline "userProfile"}}
        <div class="user-profile">
            <h2>{{name}}</h2>
            <p>Age: {{age}}</p>
        </div>
    {{/inline}}

    <h1>User Profiles</h1>
    {{> userProfile name="John Doe" age=30 }}
    {{> userProfile name="Jane Doe" age=25 }}
</body>
</html>
//호출결과
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Inline Partials Example</title>
</head>
<body>
    <h1>User Profiles</h1>
    <div class="user-profile">
        <h2>John Doe</h2>
        <p>Age: 30</p>
    </div>
    <div class="user-profile">
        <h2>Jane Doe</h2>
        <p>Age: 25</p>
    </div>
</body>
</html>

Block Helpers

Helper는 기본적으로 다른 개발언어의 함수라고 볼 수 있다.

Basic Blocks

<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{#noop}}{{body}}{{/noop}}
  </div>
</div>
Handlebars.registerHelper("noop", function(options) {
  return options.fn(this);
});
  
//결과
{{./noop}}

registerHelper - 사용자정의 헬퍼를 등록하는 함수
noop ("no operation"의 줄임말) - 아무작업도 수행하지 않는 헬퍼 {{#noop}}
function(options) - 헬퍼함수는 하나의 매개변수 ('options')를 받음
options - 다양한 속성과 메서드가 포함되어 있음 (주로, 'fn','inverse'가 사용됨
options.fn(this) - 여기서 this는 현재 context

✔️ options.fn과 options.inverse의 차이
Block Helper를 정의할때 사용하는 두가지 중요한 메서드

options.fn - Block Helper내부의 기본 콘텐츠를 렌더링하는 함수
options.inverse - Block Helper가 '{{else}}'블록을 포함할때 사용됨
'option.inverse'는 Block Helper내부의 '{{else}}'부분을 렌더링한다.

//예제
Handlebars.registerHelper("check",function(condition,option){
	if(condition){
		return options.fn(this);
	}else{
		return options.inverse(this);
	}
});

//심화 예제
Handlebars.registerHelper("list",function(items,option){
  Const itemAsHtml = items.map(item=> "<li>" + options.fn(item) + "<li>");
  return "<ul>\n" + itemAsHtml.join("\n") + "\n</ul>";
});

Handlebars.registerHelper("list",function(items,option)

"list" - 첫번째 인수
function(items,option) - 두번째 인수 (헬퍼함수)
items,option - 두개의 매개변수

Const itemAsHtml = items.map(item=> "<li>" + options.fn(item) + "<li>");

items - 헬퍼가 호출될때 전달되는 배열
map - 배열의 각 요소에 대해 주어진 함수를 호출하고, 그결과를 새로운 배열로 반환
options.fn(item) - 현재 item을 context로 하여 Block Helpers내부의 템플릿을 렌더링한다.

map변환후
"< li>Item1</ li>","< li>Item2</ li>","< li>Item3</ li>"

return "<ul>\n" + itemAsHtml.join("\n") + "\n</ul>"

\n - 줄바꿈요소
.join() - join메서드, 배열의 모든요소를 하나의 문자열로 결합하고 각요소 사이에 줄바꿈문자를 삽입하는 부분
itemAsHtml.join("\n") - 사용하는 이유
① 각 <li>요소를 줄바꿈 문자 (\n)로 결합하여 HTML코드가 더 읽기 쉽게 하기 위함.
② HTML이 여러줄로 깔끔하게 정렬되어 유지보수와 디버깅에 용이

join사용후
"< li>Item1</ li>\n< li>Item2</ li>\n< li>Item3</ li>"

최종 HTML 결과

<ul>
  <li>Item1</li>
  <li>Item2</li>
  <li>Item3</li>
</ul>

Basic Block Variation

<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{#noop}}{{body}}{{/noop}}
  </div>
</div>
Handlebars.registerHelper("bold", function (options) {
  return new Handlebars.SafeString('<div class="mybold">' + options.fn(this) + "</div>");
});

The with helper

<div class="entry">
  <h1>{{title}}</h1>
  {{#with story}}
    <div class="intro">{{{intro}}}</div>
    <div class="body">{{{body}}}</div>
  {{/with}}
</div>
{
  title: "First Post",
  story: {
    intro: "Before the jump",
    body: "After the jump"
  }
}
  
Handlebars.registerHelper("with", function(context, options) {
  return options.fn(context);
});

with헬퍼 사용부분 : {{#with story}} ... {{/with}}

  • #with story객체가 context로 전달된다.
  • option.fn(context)는 블록내부의 내용을 렌더링 하며 이때, context는 story객체이다.
//출력값
<div class="entry">
  <h1>First Post</h1>
  <div class="intro">Before the jump</div>
  <div class="body">After the jump</div>
</div>

Simple Iterators
-
Conditionals
If / Unless
if
조건문이 false일때는 else블록도우미에 일반기능을 제공하여 문제처리한다.
또다른 조건문이 생겼을때 (후속 helper) else if도 사용가능

Hash Arguments
-
Block Parameters

{{#each users as |user userId|}}
  Id: {{userId}} Name: {{user.name}}
{{/each}}

user = userId

Raw Blocks
처리되지 않은 콧수염블록을 처리해야하는 템플릿에 Raw Blocks을 사용할 수 있다.

{{{{raw-loud}}}}
  {{bar}}
{{{{/raw-loud}}}}
Handlebars.registerHelper('raw-loud', function(options) {
    return options.fn().toUpperCase()
});

//출력값
{{BAR}}

블록내부의 콘텐츠를 대문자로 변환하여 렌더링해라


Built-in Helpers

• #If
조건부로 렌더링할때 사용한다.

return값을 false, undefined, null, "", 0, [] 로 반환하는 경우, 블록은은 렌더링하지 않는다.

**①번 (if가 참일 경우)**
<div class="entry">
{{#if author}}
<h1>{{firstName}} {{lastName}}</h1>
{{/if}}
</div>

//input
{
  author: true,
  firstName: "Yehuda",
  lastName: "Katz",
}

//output
<div class="entry">
<h1>Yehuda Katz</h1>
</div>

if값이 true여서 output을 출력한다.

**②번 (if가 거짓일 경우(비었을 경우))**

input값이 비었으면 (null)

//output
<div class="entry"></div>

👉블록표현식으로 사용할 때 표현식이 falsy값을 반환하는 경우
else로 표현할 수 있음

<div class="entry">
{{#if author}}
<h1>{{firstName}} {{lastName}}</h1>
{{else}}
<h1>Unknown Author</h1>
{{/if}}
</div>

//input
{
  author: false,
  firstName: "Yehuda",
  lastName: "Katz",
}

//output
<div class="entry">
<h1>Unknown Author</h1>
</div>

**includeZero
includeZero=true옵션은 조건을 비어있지 않는 것으로 처리하도록 설정할 수 있음
(이는 0 양수 or 음수로 처리여부를 판별)

{{#if 0 includeZero=true}}
<h1>Does render</h1>
{{/if}}

** Sub-Expressions (하위표현식)
템플릿에 사용자 지정논리를 추가하기 위해 제안된 방법

#if는 false를 반환하므로 적합하지 않을 수 있음
👉 undefined를 사용

//예제

//template
{{#if (isdefined value1)}}true{{else}}false{{/if}}
{{#if (isdefined value2)}}true{{else}}false{{/if}}

//script
Handlebars.registerHelper('isdefined', function (value) {
  return value !== undefined;
});

//input
{ value1: {} }

//output
true
false

input에 value1에 대한 값만 구했으니, true를 반환하고,
value2값은 없으니 false를 반환한다.

• #Unless
if의 반대로 unless를 사용한다.

표현식이 거짓값을 반환하면 해당블록이 렌더링된다.

<div class="entry">
{{#unless license}}
<h3 class="warning">WARNING: This entry does not have a license!</h3>
{{/unless}}
</div>

//input
{}

//output
<div class="entry">
<h3 class="warning">WARNING: This entry does not have a license!</h3>
</div>

• #each
each헬퍼를 사용하여 목록을 반복할 수 있음

<ul class="people_list">
  {{#each people}}
    <li>{{this}}</li>
  {{/each}}
</ul>

//input
{
  people: [
    "Yehuda Katz",
    "Alan Johnson",
    "Charles Jolley",
  ],
}

//output
<ul class="people_list">
    <li>Yehuda Katz</li>
    <li>Alan Johnson</li>
    <li>Charles Jolley</li>
</ul>

else는 목록이 비어있을때 선택적으로 사용가능하다

{{#each paragraphs}}
<p>{{this}}</p>
{{else}}
<p class="empty">No content</p>
{{/each}}

//input
{} //값이비었음

//output
<p class="empty">No content</p>

**항목 반복 (현재 loop index를 참조){{@index}}

{{#each array}} {{@index}}: {{this}} {{/each}}

**객체 반복 (현재 loop index를 참조) {{@key}}

{{#each object}} {{@key}}: {{this}} {{/each}}

반복의 첫번째와 마지막단계에서 배열반복은 @first / @last로 표시

중첩된 each블록은 반복변수을 통해 기반경로로 접근할수있다.
(ex. 상위index에 접근하려면, {{@../index}}로 사용)

• #with
with helper는 template-part의 평가 context를 변경할 수 있음

{{#with person}}
{{firstname}} {{lastname}}
{{/with}}

//input
{
  person: {
    firstname: "Yehuda",
    lastname: "Katz",
  },
}

//output
Yehuda Katz

with는 현재블록에 참조를 정의하기 위해 매개변수와 함께 사용할수 있음

//예제
{{#with city as | city |}}
  {{#with city.location as | loc |}}
    {{city.name}}: {{loc.north}} {{loc.east}}
  {{/with}}
{{/with}}
  
//input
{
  city: {
    name: "San Francisco",
    summary: "San Francisco is the <b>cultural center</b> of <b>Northern California</b>",
    location: {
      north: "37.73,",
      east: -122.44,
    },
    population: 883305,
  },
}

//output
San Francisco: 37.73, -122.44

../ - 템플릿이 더 명확한 코드를 제공할 가능성이 있다.
{{else}} - 전달값이 비어있는 경우 선택적 제공가능

{{#with city}}
{{city.name}} (not shown because there is no city)
{{else}}
No city found
{{/with}}

//input
{
  person: {
    firstname: "Yehuda",
    lastname: "Katz",
  },
}

//output
No city found

city name이 없기때문에 false를 반환하는데, else가 있어 else값을 반환한다.

• lookup
동적매개변수 확인을 도와준다 (배열 인덱스의 값을 확인에 유용)

{{#each people}}
   {{.}} lives in {{lookup ../cities @index}}
{{/each}}

//input
{
  people: ["Nils", "Yehuda"],
  cities: [
    "Darmstadt",
    "San Francisco",
  ],
}

//output
Nils lives in Darmstadt
Yehuda lives in San Francisco

또한, 입력의 데이터를 기반으로 객체의 속성을 조회하는데 사용가능

as || - ||을 별칭으로 사용한다.

//고급예제
{{#each persons as | person |}}
    {{name}} lives in {{#with (lookup ../cities [resident-in])~}}
      {{name}} ({{country}})
    {{/with}}
{{/each}}

//input
{
  persons: [
    {
      name: "Nils",
      "resident-in": "darmstadt",
    },
    {
      name: "Yehuda",
      "resident-in": "san-francisco",
    },
  ],
  cities: {
    darmstadt: {
      name: "Darmstadt",
      country: "Germany",
    },
    "san-francisco": {
      name: "San Francisco",
      country: "USA",
    },
  },
}
//output
Nils lives in Darmstadt (Germany)
Yehuda lives in San Francisco (USA)

{{#each persons as | person |}} - persons를 person으로 별칭한다.
{{name}} - 현재 person 객체의 name 속성을 출력
{{#with (lookup ../cities [resident-in])~}} -

▸ lookup헬퍼를 사용하여 cities객체에서 현재 person의 resident-in 속성 값에 해당하는 도시 객체를 찾음
../cities는 상위 context의 cities객체를 참조
[resident-in] - 현재 person 객체의 resident-in 속성 값을 동적으로 사용하여 cities객체에서 해당도시를 조회한다
~ 문자는 공백 제거를 의미

• log
log를 사용하면 템플릿의 context상태를 기록할수 있음
재정의 - Handlebars.logger.log

//지원되는 값 (debug,info,warn,error,info logging is the default)
{{log "debug logging" level="debug"}}
{{log "info logging" level="info"}}
{{log "info logging is the default"}}
{{log "logging a warning" level="warn"}}
{{log "logging an error" level="error"}}

조건부 - Handlebars.logger.level 기본값 - info


03. 적용하기

가장 기본적인 바인딩 구조

<script id="entry-template" type="text/x-handlebars-template">
<table>
    <thead> 
        <th>이름</th> 
        <th>아이디</th> 
        <th>메일주소</th> 
    </thead> 
    <tbody> 
        {{#users}} 
        <tr> 
            <td>{{name}}</td> 
            <td>{{id}}</td> 
            <td><a href="mailto:{{email}}">{{email}}</a></td> 
        </tr> 
        {{/users}} 
    </tbody> 
</table>
</script>

<script>태그의 type속성을 살펴보면 text/x-handlebars-template을 통해 핸들바 템플릿을 사용한다는 것을 알 수 있다.
더불어, {{#users}}{{/users}}로 감싸진 부분은 users라는 배열의 길이만큼 반복된다. {{name}}처럼 내부에 괄호로 감싸진 부분은 배열 요소 값으로 바뀌는 부분이다.

앞서 말했듯이, Handlebars는 자바스크립트함수로 컴파일된다. 예제의 js파일을 살펴보자.

//핸들바 템플릿 가져오기
var source = $("#entry-template").html(); 

//핸들바 템플릿 컴파일
var template = Handlebars.compile(source); 

//핸들바 템플릿에 바인딩할 데이터
var data = {
    	users: [
            { name: "홍길동1", id: "aaa1", email: "aaa1@gmail.com" },
            { name: "홍길동2", id: "aaa2", email: "aaa2@gmail.com" },
            { name: "홍길동3", id: "aaa3", email: "aaa3@gmail.com" },
            { name: "홍길동4", id: "aaa4", email: "aaa4@gmail.com" },
            { name: "홍길동5", id: "aaa5", email: "aaa5@gmail.com" }
        ]
}; 

//핸들바 템플릿에 데이터를 바인딩해서 HTML 생성
var html = template(data);

//생성된 HTML을 DOM에 주입
$('body').append(html);

id를 통해 핸들바 템플릿을 가져오고, 컴파일이 이루어진다. 변수 html에 파라미터로 데이터를 넣어주고 JQuery로 body끝에 html을 추가하였다.

참조
https://handlebarsjs.com/
https://velog.io/@somin_0/Handlebars
https://programmingsummaries.tistory.com/381
https://ijbgo.tistory.com/7
https://velog.io/@hanblueblue/프로젝트1-3.-Handlebars-이용해-프론트-템플릿-만들기
https://velog.io/@parkoon/실무에서-Handlebars-사용하기-feat-express
https://enai.tistory.com/22
+thanks to chatGPT

profile
뚝딱뚝딱 열심히 공부 중 이예요!

0개의 댓글