본문 바로가기
Old Posts/Java

[Java] 프리마커 템플릿 언어(FTL, Freemarker Template Language) 문법

by A6K 2021. 7. 15.

자바 템플릿 엔진(Java Template Engine)인 프리마커(Freemarker)는 FTL(Freemarker Template Language)라는 언어를 통해 템플릿을 작성할 수 있도록 한다. FTL에는 간단한 데이터 치환뿐만 아니라 조건문이나 반복문 같은 다양한 문법과 기능이 구현되어 있다. 따라서 FTL 문법을 잘 알고 있으면 효율적으로 템플릿을 작성할 수 있다.

FTL의 모든 문법이나 정확한 기능에 대해서는 Freemarker 매뉴얼을 참고하도록 하자. 이 포스트에서는 자주 사용하는 FTL 문법과 기능을 소개하고 간단한 사용법을 예제를 통해 설명하겠다.

assign

템플릿 작성시 변수를 사용하고 싶을 때, assign 디렉티브를 사용할 수 있다. 예를 들어

<#assign name="Dave">

<#assign codeBlock>
     this code block assigned
</#assign>

assign 디렉티브를 이용해 'name'이라는 변수에 "Dave"라는 값을 저장할 수 있다. 이후 ${name}을 사용하면 name 변수에 저장된 "Dave"라는 값이 ${name} 부분에 치환된다.

간단한 문자열, 단어뿐아니라  <#assign>과 사이에 있는 코드 블럭을 변수처럼 할당해서 사용할 수도 있다.

attempt, recover

자바에서 사용하는 try-catch 블럭과 유사한 개념이다.

<#attempt>
    attempt block
<#recover>
    recover block
</#attempt>

attempt block의 템플릿이 우선 해석된다. 그러다가 예외가 발생하거나 문제가 생기면 attempt block에서 진행되던 작업은 모두 롤백되고 recover block의 내용이 해석된다.

자바의 Try-Catch 구문처럼 여러번 중첩해서 사용할 수 있다.

autoesc

오토 이스케이핑(Auto Escaping)과 관련된 기능이다.

<#autoesc>
    ...
</#autoesc>

웹 페이지와 관련된 데이터를 다루다보면 XML, HTML 같은 문서에서 이스케이핑 해야하는 문자들이 있다. 예를 들어 '<' 문자나 '>' 문자는 XML, HTML 문서에서 특수한 역할을 한다. 만약 이 문자를 문자 그대로 사용하고 싶은 경우라면 별도로 이스케이핑을 해야한다. (이런 문자들은 이스케이핑해서 엔티티로 만들어 사용해야한다)

일반적으로는 freemarker.core.OutputFormat에 HTML, XML, XHTML 등을 설정하거나 text/html, application/xml 같은 MIME 타입을 설정해주면 자동으로 Escaping이 실행되기 때문에 이 디렉티브는 잘 사용하지 않는다.

${expression?esc}

참고로 Auto Escaping 기능이 활성화 되지 않은 상황에서 하나의 Interpolation을 Escaping 하려면 "?esc"를 붙여주면 된다.

compress

불필요한 공백문자(White-space)를 결과에서 제거하는데 사용된다.

<#compress>
   ...
</#compress>

HTML이나 XML같이 공백 문자가 주요하게 사용되는 경우 사용자 데이터에서 불필요한 공백문자를 지울 필요가 있는데, 이 때 '<#compress>' 디렉티브를 사용하면 된다.

이 디렉티브를 사용하면 <#compress> 블럭 내에서 2개 이상의 공백 문자가 반복해서 등장하는 경우를 감지해서 하나의 공백문자로 줄여준다. 줄바꿈 문자도 여러개가 등장하면 하나로 줄여준다. 맨 처음과 맨 마지막에 등장하는 공백과 줄바꿈 문자들은 제거된다.

flush

프리마커 템플릿 엔진이 처리한 결과는 process() 메소드에서 받은 Output Writer로 전송된다. 성능 향상을 위해 데이터 모델과 함께 처리되어 만들어진 텍스트는 바로바로 Output Writer로 전송되지 않고 버퍼링을 거친다. 대부분 버퍼링과 플러시(Flush)는 적당한 때에 자동으로 발생한다. 하지만 템플릿의 특정 부분에서 명시적으로 "지금까지 처리한 결과를 플러시(Flush) 해줘!!")라고 지시하고 싶은 경우가 있다. 이럴 때 <#flush> 디렉티브를 사용하면된다.

프리마커 템플릿 엔진은 템플릿을 처리하다가 <#flush> 디렉티브를 만다면 할당된 java.io.Writer 인스턴스의 flush() 메소드를 호출한다. 웹 페이지의 경우 빨리빨리 처리되어야 하는 부분을 먼저 플러시하도록 이 기능을 사용할 수 있다.

ftl

템플릿 파일의 맨 처음부분에 사용되는 디렉티브다. 템플릿 파일에 대한 정보를 프리마커 엔진에 알려주는 역할을 한다.

<#ftl param1=value1 param2=value2 ... >

param1, param2 같은 특정 파라미터의 값을 설정하는 디렉티브로  'encoding', 'strip_whitespace', 'strip_text' 등이 사용될 수 있으며, 값으로는 'ISO-8859-5' 같은게 사용될 수 있다.

function, return

템플릿 파일 내부에서 사용할 함수를 정의한다. 'macro' 디렉티브와 비슷하지만 function 디렉티브에서는 반드시 return 디렉티브로 반환 값을 명시해야 한다.

<#function name param1 param2 ... paramN>
    ...
    <#return returnValue>
    ...
</#function>

function 디렉티브 내에서 만들어내는 문서 출력은 모두 무시가 된다. 내부 로직에 의해서 값을 계산하고 return 디렉티브로 결과를 반환하는 작업만 수행된다. function 디렉티브 내부에서 if, list 등의 다른 디렉티브는 사용할 수 있다.

global

전역 변수를 만들어 낸다.

<#global name=value>
or
<#global name1=value1 name2=value2 ... nameN=valueN>
or
<#global name>
    capture this
</#global>

<#assign> 디렉티브와 비슷한 역할을 한다. 다만 assign 디렉티브로 만들어진 변수는 모든 네임스페이스(namespace)에서 보이게 된다.

global 디렉티브로 정의한 변수의 이름이 assign으로 정의한 변수의 이름과 같다면 assign으로 정의한 변수가 유효한 네임스페이스에서는 global 디렉티브로 정의한 변수 대신 assign 으로 정의한 변수가 보이게 된다. 변수의 스코프에 대해서 잘 알고 사용해야 한다.

if, else, elseif

템플릿에서 사용할 수 있는 조건문이다.

<#if condition>
   ...
<#elseif condition2>
   ...
<#elseif condition3>
   ...
<#else>
   ...
</#if>

자바에서 사용하는 것처럼 여러 조건에 따라 다른 처리를 할 수 있다. 맨 위에서부터 <#if>, <#elseif>, <#else> 순으로 처리된다.

조건문에서 값의 비교를 할 때 주의해야 할 점이 있다. 바로 "x > 0" 혹은 "x >= 0" 같은 조건식을 사용할 때, 에러가 발생한다. '>' 문자는 프리마커에서 디렉티브를 나타내는 특수문자이기 때문이다. 크다 작다를 '>', '<' 문자대신 gt, gte, lt, lte 로 표현해야한다. 즉, <#if x gt 0> 혹은 <#if x gte 0>로 작성해야 한다. 만약 이런 비교가 괄호 안에서 발생한다면 문제는 없다. 즉, <#if foo.bar(x > 0)>는 문제 없이 동작한다.

import

다른 템플릿 파일의 내용을 불러온다.

<#import path as hash>

path에 해당하는 템플릿 파일의 내용을 불러들여 수행한 다음 hash라는 이름의 네임스페이스로 만들어준다. path에 해당하는 템플릿에서 만들어진 다양한 변수와 객체들은 hash라는 네임스페이스를 통해서 접근할 수 있다.

예를 들면,

<#import "/tmp/test.ftl" as my>
<@my.macro x="abc"/>

<#import> 디렉티브를 이용해서 "/tmp/test.ftl"이라는 템플릿 파일에 정의되어 있는 FTL 내용들을 불러들여 my라는 네임스페이스로 만들어주었다. 이후 "@네임스페이스"를 통해 불러들인 함수나 변수, 매크로 등을 사용할 수 있다.

만약 같은 템플릿 파일을 여러번 import 한다면 첫 번째 import만 수행된다. 두 번째 import부터는 hash만 생성되고 같은 네임스페이스를 참조하게 된다. import를 이용해서 템플릿을 수행하면서 만들어진 출력은 무시된다.

include

import 디렉티브처럼 다른 템플릿을 불러온다. 차이점은 불러오는 템플릿의 변수와 매크로 등을 공유한다는 점이다. 다시말해서 같은 네임스페이스로 불러들인다는 말이다.  불러오는 템플릿에서 출력되는 Output 내용은 include 태그가 사용된 곳으로 복사된다. include는 다른 템플릿의 내용을 복사-붙여넣기 하는 개념과 비슷하다. 

<#include path> 
or 
<#include path options>

include 디렉티브로 같은 템플릿을 여러번 불러올 경우 여러번 처리된다. include 디렉티브에는 옵션을 줄 수도 있다. 사용할 수 있는 옵션에는 'parse', 'encoding', 'ignore_missing' 등이 있으며, 자세한 내용은 매뉴얼을 참고하자.

list, else, items, sep, break, continue (반복문)

데이터 모델의 자식 노드들을 순회하는 반복문과 관련된 디렉티브들이다.

<#list sequence as item>
    list block here
</#list>

가장 간단한 형태의 반복문이다. sequence 라는 이름의 컬렉션 데이터의 각 항목들을 순회하는 코드로 list 블럭 내부에서 각 아이템 항목을 item 이라는 변수로 접근할 수 있다.

예를 들어 ['Dave', 'Tom', 'Nancy'] 라는 users 변수가 있을 때,

<#list users as user>
    ${user}
</#list>

라고 사용하면, users에 들어있는 3명의 이름이 각각 user에 할당되어 실행된다.

<#list hash as key, value>
    list block here
</#list>

만약 해시 맵을 순회하고 싶으면 위 코드처럼 as 뒤에 key, value를 적어주면 된다. 그리고 list 블록안에서 ${key}, ${value} 형태로 참조해서 사용하면된다. (key, value 말고 다른 변수 이름을 명시해도 좋다)

<#list sequence as item>
    Part repeated for each item
<#else>
    Part executed when there are 0 items
</#list>

list 디렉티브에는 else 디렉티브가 짝으로 붙을 수 있다. list 디렉티브로 순회 할 대상이 없는 경우, 즉 sequence에 해당하는 데이터 모델이 비어있는 경우엔 else 디렉티브의 템플릿 블럭이 실행된다.

<#list sequence>
Part executed once if we have more than 0 items
<#items as item>
Part repeated for each item
</#items>
Part executed once if we have more than 0 items
<#else>
Part executed when there are 0 items
</#list>

이렇게 사용하는 경우도 있다. list 디렉티브를 시작하기 전과 후에 실행할 내용을 표현하고 싶은 경우 items 디렉티브를 list 디렉티브 안쪽에 사용하는 식으로 쓰면 된다.

seq

List 디렉티브 내부에서 사용할 수 있는 디렉티브 중에 seq 디렉티브가 있다. 컬렉션 데이터를 출력할 때, 구분자로 사용하고 싶은 문자를 넣을 때 사용된다.

<#list users as user>
${user}<#sep>, </#sep>
</#list

이렇게 명시하면 각 user의 출력 사이에 콤마(',') 문자를 준다. seq 디렉티브가 편한 이유는 마지막 항목에서는 자동으로 구분자를 빼주기 때문이다.

break

List 를 순회하다가 특정 조건을 만났을 때, 순회를 종료하려고 할 때 사용된다. 자바의 break; 와 동일하다

continue

list 템플릿 블럭의 이후 부분을 건너뛰고 다음 순회로 넘어가려고 할 때 사용한다. 마찬가지로 자바의 continue; 와 동일하다.

local

assign 디렉티브와 비슷하게 변수를 할당하는 디렉티브다.

<#local name=value>
or
<#local name1=value1 name2=value2 ... nameN=valueN>
or
<#local name>
capture this
</#local>

하지만 local 디렉티브는 macro 디렉티브와 function 디렉티브의 안쪽에서만 사용할 수 있다는 제한이 있다.

macro, nested, return

매크로(macro) 변수를 생성한다. 매크로는 템플릿 파일의 조각으로 사용자 정의 디렉티브(User-defined directive)로 사용될 수 있다.

<#macro name param1 param2 ... paramN>
...
<#nested loopvar1, loopvar2, ..., loopvarN>
...
<#return>
...
</#macro>

매크로의 사용처가 매크로의 정의보다 먼저 등장해도 상관없다. 하지만 매크로의 정의가 include 디렉티브에 의해서 포함되는 경우라면, 매크로의 사용처보다 include 디렉티브가 먼저 등장해야한다.

nested

nested 디렉티브는 <#macro>와 </#macro> 사이에 있는 템플릿 조각을 실행하는데 사용된다.

<#macro foo >
  * <#nested>
  * <#nested>
</#macro>

<@foo>Hello</@foo>

이 템플릿의 경우, nested 부분에 Hello 가 대체되어서 출력된다.

nested는 loop 변수를 포함할 수 있다.

예를 들어

<#macro foo>
   <#list 1..5 as i>
      <#nested i, i * 2, i/2>
   </#list>
</#macro>

<@foo ; first, second, thrid>
i = ${first}
i * 2 = ${second}
i / 2 = ${third}
</@foo>

매크로의 정의에 있는 loop 변수는 3개로 각각 i, i * 2, i / 2다. 이 들은 사용자 정의 디렉티브에서 사용될 때, 각각 first, second, thrid라는 변수에 할당되어서 사용된다.

return

return 디렉티브를 만나면 macro, function의 실행을 종료한다.

noautoesc

오토 이스케이핑 기능을 사용하지 않는 구역을 만든다. autoesc 디렉티브의 반대 개념이라고 생각하면 된다.

<#noautoesc>
...
</#noautoesc>

하나의 interpolation에만 적용하려면 ${expression?no_esc}로 사용하면 된다.

noparse

프리마커 언어로 해석하지 않는 구역을 만든다.

<#noparse>
...
</#noparse>

noparse 구역에서는 프리마커 언어의 문법이 해석되지 않는다. 다만 </#noparse> 구문은 해석된다.

nt

No-Trim의 약자. 이 디렉티브가 있는 라인은 공백 문자를 Stripping을 하지 않는다

outputformat

출력 포맷 설정을 변경한다.

<#outputformat formatName>
...
</#outputformat>

formatname으로는 "HTML", "XML" 등이 사용될 수 있다. outputformat 참조 output 포맷을 적절하게 설정하면, 그 포맷에서 사용하는 특수 문자들을 자동으로 이스케이핑 해준다.

outputformat 디렉티브가 끝나면, 이전에 설정되었던 outputformat으로 자동 복구된다.

setting

이후 프로세싱에 영향을 미칠 설정 값들을 변경한다.

<#setting name=value>

이 설정값들은 프리마커 엔진의 해석 방식에 영향을 주게 된다.

예를 들어, 이 디렉티브로 locale, number_format, boolean_format, date_format 등을 변경하게 되면, 이 후에 프리마커 엔진이 데이터를 처리할 때마다 여기에서 바꾼 설정값에 따라 움직이게 된다.

setting 디렉티브로 변경할 수 있는 설정값은 매뉴얼을 참조하도록 하자.

stop

템플릿의 프로세싱을 종료한다.

<#stop>
or
<#stop reason>

에러메시지를 줄 수도 있다. 일반적이지 않은 에러 상황에서만 사용해야 한다.

switch, case, default, break

자바의 switch, case 문과 동일하다.

<#switch value>
<#case refValue1>
...
<#break>
<#case refValue2>
...
<#break>
...
<#case refValueN>
...
<#break>
<#default>
...
</#switch>

value의 값에 따라 수행해야하는 블럭이 결정된다.

t, lt, rt

공백문자의 Trim 동작에 대한 디렉티브다. 이 디렉티브가 있는 라인은 다음과 같이 동작한다.

  • t : 앞 뒤 공백문자를 제거한다.
  • lt : 왼쪽에 있는 공백 문자를 제거한다. (공백문자로 시작)
  • rt : 오른쪽에 있는 공백 문자를 제거한다. (공백문자들로 종료)

사용자 정의 디렉티브(User-defined directive)

매크로에서 설명한 내용과 동일하다. 시스템 디렉티브와 다르게 골뱅이(@) 문자로 시작한다.

visit, recurse, fallback

재귀적인 방법으로 트리를 순회하며 데이터를 처리하고 싶을 때 사용하는 디렉티브, 실제로는 XML 데이터를 다룰 때 많이 사용한다.

<#visit node using namespace>
or
<#visit node>
<#recurse node using namespace>
or
<#recurse node>
or
<#recurse using namespace>
or
<#recurse>
<#fallback>

<#visit node>를 호출하면 node?node_name 에 해당하는 매크로를 찾아서 실행시켜준다. 만약 node_name에 해당하는 매크로가 없으면, @node_type 이름을 갖는 매크로를 찾아서 실행한다. 만약 이것도 없으면 템플릿 프로세싱은 에러를 내면서 종료한다.

<#recurse node using ns>는 <#list node?children as child><#visit child using ns></#list>와 동일한 동작을 한다. 짧게 쓰기 위해 사용한다.

visit 디렉티브에 의해서 사용자 정의 디렉티브가 찾아진다. 이 때, fallback 디렉티브를 만나면, 다른 네임스페이스에서 node?node_name에 해당하는 매크로를 찾아보도록 프리마커 엔진에게 알려준다. 같은 이름의 매크로를 오버라이드해서 사용하는 경우 적당한 처리를 위해서 사용할 수 있다.

대략적인 프리마커 템플릿 엔진의 문법은 이와 같다. 이 포스트에서 다루고 있는 내용은 정말 "대략적인" 것이니 상세한 예제와 사용법은 프리마커 매뉴얼을 찾아보길 바란다.

댓글