이제 database에 있는 데이터를 바탕으로 화면을 꾸며보도록 하자.
showSnippet
핸들러에 다음의 코드를 추가하여 show.page.tmpl
template file을 랜더링하도록 하자.
...
func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil || id < 1 {
app.notFound(w)
return
}
s, err := app.snippets.Get(id)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
app.notFound(w)
} else {
app.serverError(w, err)
}
return
}
// Initialize a slice containing the paths to the show.page.tmpl file,
// plus the base layout and footer partial that we made earlier.
files := []string{
"./ui/html/show.page.tmpl",
"./ui/html/base.layout.tmpl",
"./ui/html/footer.partial.tmpl",
}
// Parse the template files...
ts, err := template.ParseFiles(files...)
if err != nil {
app.serverError(w, err)
return
}
// And then execute them. Notice how we are passing in the snippet
// data (a models.Snippet struct) as the final parameter.
err = ts.Execute(w, s)
if err != nil {
app.serverError(w, err)
}
}
...
아직 show.page.tmpl
페이지가 없으니 이제 만들어주도록 하자.
HTML templates에 넘어간 어떠한 dynamic data들 모두 .
(dot)으로 접근가능하다.
우리는 err = ts.Execute(w, s)
을 통해서 models.Snippet
구조체를 넣어주었다. 구조체의 field
의 value
는 {{.field}}
이런 식으로 접근이 가능하다. 따라서 model.Snippet
구조체의 Title
field는 {{.Title}}
로 우리의 template에서 접근이 가능하다.
ui/html/show.page.tmpl
를 만들고 다음의 코드를 추가하자.
touch ui/html/show.page.tmpl
{{template "base" .}}
{{define "title"}}Snippet #{{.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
</div>
{{end}}
application을 다시 시작하고 다음의 url로 접속하면 datebase로 부터 데이터를 가져와 template에 담아 표현해줄 것이다.
http://localhost:4000/snippet?id=1
err = ts.Execute(w, s)
에서 Execute
메서드를 확인해보자.
func (t *Template) Execute(wr io.Writer, data any) error {
if err := t.escape(); err != nil {
return err
}
return t.text.Execute(wr, data)
}
오직 하나의 data
만을 매개변수로 받는 것을 확인할 수 있다. 여러 개의 data를 보내어 template를 꾸미고 싶다면 어떻게 해야할까??
가장 좋은 방법은 하나의 template
type를 만들어 렌더링하고 싶은 여러 개의 타입을 넣는 방식이다.
cmd/web/templates.go
파일을 새로만들어 templateData
구조체가 위의 일을 하도록 하자.
touch cmd/web/templates.go
다음의 코드를 넣어주도록 하자.
package main
import "github.com/gyu-young-park/snippetbox/pkg/models"
type templateData struct {
Snippet *models.Snippet
}
handlers.go
의 showSnippet
에 해당 구조체를 사용해보도록 하자.
func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil || id < 1 {
app.notFound(w)
return
}
s, err := app.snippets.Get(id)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
app.notFound(w)
} else {
app.serverError(w, err)
}
return
}
// Create an instance of a templateData struct holding the snippet data.
data := &templateData{Snippet: s}
files := []string{
"./ui/html/show.page.tmpl",
"./ui/html/base.layout.tmpl",
"./ui/html/footer.partial.tmpl",
}
ts, err := template.ParseFiles(files...)
if err != nil {
app.serverError(w, err)
return
}
// Pass in the templateData struct when executing the template.
err = ts.Execute(w, data)
if err != nil {
app.serverError(w, err)
}
}
우리의 데이터인 models.Snippets
구조체가 templateData
구조체 안에 포함되게 되었다. 이 데이터를 template에서 불러오기 위해서 다음과 같이 하면 된다.
{{template "base" .}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Snippet.Title}}</strong>
<span>#{{.Snippet.ID}}</span>
</div>
<pre><code>{{.Snippet.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Snippet.Created}}</time>
<time>Expires: {{.Snippet.Expires}}</time>
</div>
</div>
{{end}}
다시 해당 url에 접속해보도록 하자.
http://localhost:4000/snippet?id=1
이전과 동일한 페이지가 나왔다면 성공이다.
참고로 html/template
패키지는 자동적으로 {{}}
tags 사이에 생성되는 어떠한 데이터든 escape
한다. 이러한 행동은 XSS(cross-site scripting)를 피하는데 큰 도움이 된다. 때문에 text/template
대신에 html/template
패키지를 사용해야하는 이유이기도 하다.
가령 다음과 같은 동적 데이터는
<span>{{"<script>alert('xss attack')</script>"}}</span>
다음과 같이 harmlessly하게 변경되어 렌더링된다.
<span><script>alert('xss attack')</script></span>
html/template
패키지는 굉장히 스마트하여 어떠한 데이터가 html, css, js, uri 등에서 렌더링되냐에 따라 sequence들을 적절하게 escape한다.
Nested template를 하고 싶다면, {{template}}
또는 {{block}}
action을 사용하면 된다. 이때 {{}}
태그 안에 마지막은 .
로 적어주어야 한다.
{{template "base" .}}
{{template "main" .}}
{{template "footer" .}}
{{block "sidebar" .}}{{end}}
또한, template에서 method를 호출하고 싶다면 다음과 같이 사용이 가능하다. 가령, .Snippet.Created
에서 time.Time
타입이 가진 Weekday()
메서드를 호출하고 싶다면 Weekday()
메서드를 다음과 같이 호출하면 된다.
<span>{{.Snippet.Created.Weekday}}</span>
만약, 매개변수가 있다고 한다면 다음과 같이 써주면 된다. AddDate()
메서드로 6개월을 더하고 싶다면 다음과 같이 하자.
<span>{{.Snippet.Created.AddDate 0 6 0}}</span>
단, 이렇게 template안에서 method를 호출하는 것은 single return value일 때만 가능하다.
이전부터 {{define}}, {{template}}, {{block}}
과 같은 action
들을 사용해왔는데, 이것 말고도 {{if}}
, {{with}}
, {{range}}
등의 action들로 dynamic data를 control할 수 있다.
{{if .Foo}} C1 {{else}} C2 {{end}}
는 .Foo
가 있다면 C1
이 렌더링되고, 그렇지않다면 C2
가 렌더링된다.
{{with .Foo}} C1 {{else}} C2 {{end}}
는 .Foo
가 있다면 .Foo
값을 설정하고 C1을 렌더링한다. 그렇지않으면 C2
를 렌더링한다.
{{range .Foo}} C1 {{else}} C2 {{end}}
.Foo
의 length가 zero보다 크면 loop를 돌면서 각 값들을 값으로 설정하고 C1의 내용을 렌더링한다. 만약 .Foo
의 길이가 0이면 C2
내용이 렌더링된다. 따라서 .Foo
는 반드시 배열, 슬라이스, 맵 또는 채널이 되어야 한다.
여기에는 몇가지 요점이 있는데,
3개의 action모두 {{else}}
절은 optional하다. 가령, 만약 C2
내용으로 렌더링할 것이 없다면 {{if .Foo}} C1 {{end}}
로 써도 된다.
값이 없는 empty value는 false
이다. 또한, 0, nil, interface와 길이가 0인 array, slice, map, string 또한 false
이다.
with
, range
action들은 dot
의 값을 변경하는데, template에서 개발자가 의도한 대로 변경된다.
html/template
패키지는 또한 여러 template 함수를 제공하는데, 이는 런타임에 렌더링되는 것을 제어하고 개발자가 만든 template에 추가적인 로직을 추가하는 데 사용된다.
{{eq .Foo .Bar}}
는 .Foo
가 .Bar
과 같으면 true
이다.{{ne .Foo .Bar}}
는 .Foo
가 .Bar
과 다르면 true
이다.{{not .Foo}}
는 boolean
값인 .Foo
의 반대를 만들어낸다.{{or .Foo .Bar}}
는 .Foo
가 empty가 아니면 .Foo
를 yields하고 아니라면 .Bar
를 yields한다.{{index .Foo i}}
는 i
번째 .Foo
값을 yields한다. 따라서 .Foo
의 타입은 map, slice, array
이어야 한다.{{printf "%s-%s" .Foo .Bar}}
는 .Foo
, .Bar
값을 포함하는 formatted string를 만들어낸다. fmt.Sprintf
와 같다.{{len .Foo}}
: .Foo
의 길이를 integer
로 반환한다.{{$bar := len .Foo}}
는 .Foo
의 길이를 $bar
변수에 할당한다.이제 다음의 action들을 사용하여 template를 꾸며보자.
ui/html/show.page.tmpl
{{template "base" .}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
{{with .Snippet}}
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
{{end}}
</div>
{{end}}
이전과 달라진 것은 {{with .Snippet}}
과 이를 감싸는 {{end}}
tag이다. dot값은 .Snippet
에 설정된다. 즉 위에서는 .Snippet.ID
으로 templateData
struct를 가져와 사용하였는데, with
를 통해서 .Snippet
구조체를 빼온 것이다. 즉, with
안에서는 models.Snippets
구조체가 대신 사용된다는 것이다.
이제 {{if}}
와 {{range}}
action을 사용해보자.
먼저 templateData
구조체에 Snippets
필드를 slice로 잡게끔 하도록 하자.
package main
import "github.com/gyu-young-park/snippetbox/pkg/models"
type templateData struct {
Snippet *models.Snippet
Snippets []*models.Snippet
}
그리고 home
핸들러를 수정하여 우리의 database model로부터 최근의 snippet들을 불러와보자, 그리고 이 값을 home.page.tmpl
template에 넣어주도록 하자.
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
app.notFound(w)
return
}
s, err := app.snippets.Latest()
if err != nil {
app.serverError(w, err)
return
}
// Create an instance of a templateData struct holding the slice of
// snippets.
data := &templateData{Snippets: s}
files := []string{
"./ui/html/home.page.tmpl",
"./ui/html/base.layout.tmpl",
"./ui/html/footer.partial.tmpl",
}
ts, err := template.ParseFiles(files...)
if err != nil {
app.serverError(w, err)
return
}
// Pass in the templateData struct when executing the template.
err = ts.Execute(w, data)
if err != nil {
app.serverError(w, err)
}
}
s, err := app.snippets.Latest()
으로 데이터를 받아와서, data := &templateData{Snippets: s}
에 설정해주고, err = ts.Execute(w, data)
로 넘겨주는 것 말고는 딱히 수정된 것이 없다.
이제 ui/html/home.page.tmpl
파일로 가서 snippet들을 {{if}}
와 {{range}}
action을 사용하여 테이블로 보여주도록 하자.
{{if}}
action을 사용하여 snippet들의 slice가 empty인지 아닌지를 확인하고 싶다. 만약 empty라면 우리는 "There's nothing to see here yet!"
이라는 메시지를 화면에 보여줄 것이고, 그렇지 않으면 우리는 snippet 정보가 담긴 table을 렌더링할 것이다.
우리는 {{range}}
action을 사용하여 slice안의 모든 snippet들을 순회하도록 하고, 각 snippet들을 table row로 렌더링하게 할 것이다.
{{template "base" .}}
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
{{if .Snippets}}
<table>
<tr>
<th>Title</th>
<th>Created</th>
<th>ID</th>
</tr>
{{range .Snippets}}
<tr>
<td><a href='/snippet?id={{.ID}}'>{{.Title}}</a></td>
<td>{{.Created}}</td>
<td>#{{.ID}}</td>
</tr>
{{end}}
</table>
{{else}}
<p>There's nothing to see here... yet!</p>
{{end}}
{{end}}
이제 서버를 구동하여 화면이 어떻게 만들어졌는 지 확인해보자.
우리의 코드를 최적화해보도록 하자.
매번 web page를 렌더링하고 우리의 application은 관련된 template 파일을들을 template.ParseFiles()
함수를 사용하여 parse한다. 우리는 file들을 한 번만 parsing함으로서 이러한 duplicated된 작업을 피하도록 하고, parsed된 template들을 in-memory cache에 저장하도록 하자.
home
과 showSnippet
핸들러에는 duplicated된 코드들이 있다. 우리는 새로운 helper function을 만들어 이를 해결하도록 하자.
in-memory 캐시를 위해 map을 하나 만들도록 하자. map[string]*template.Template
은 pared template를 캐싱하도록 한다. cmd/web/templates.go
파일을 열어서 다음의 코드를 넣어보자.
func newTemplateCache(dir string) (map[string]*template.Template, error) {
// Initialize a new map to act as the cache.
cache := map[string]*template.Template{}
// Use the filepath.Glob function to get a slice of all filepaths with
// the extension '.page.tmpl'. This essentially gives us a slice of all the
// 'page' templates for the application.
pages, err := filepath.Glob(filepath.Join(dir, "*.page.tmpl"))
if err != nil {
return nil, err
}
// Loop through the pages one-by-one.
for _, page := range pages {
// Extract the file name (like 'home.page.tmpl') from the full file path
// and assign it to the name variable.
name := filepath.Base(page)
// Parse the page template file in to a template set.
ts, err := template.ParseFiles(page)
if err != nil {
return nil, err
}
// Use the ParseGlob method to add any 'layout' templates to the
// template set (in our case, it's just the 'base' layout at the
// moment).
ts, err = ts.ParseGlob(filepath.Join(dir, "*.layout.tmpl"))
if err != nil {
return nil, err
}
// Use the ParseGlob method to add any 'partial' templates to the
// template set (in our case, it's just the 'footer' partial at the
// moment).
ts, err = ts.ParseGlob(filepath.Join(dir, "*.partial.tmpl"))
if err != nil {
return nil, err
}
// Add the template set to the cache, using the name of the page
// (like 'home.page.tmpl') as the key.
cache[name] = ts
}
// Return the map.
return cache, nil
}
filepath.Join
을 사용하면 여러 개의 file들을 하나의 file path로 만들어준다. *.page.tmpl
가령, 디렉터리 path인 dir
과 *.page,tmpl
을 join
해주면 ui/html/*.page.tmpl
과 같이 만들어준다. 이후 filepath.Glob
을 사용해주면 *.page.tmpl
이름을 가진 모든 파일들을 slice로 가져온다.
이후, template.PaeseFiles
를 통해서 page를 template로 만들고 ts.ParseGlob()
을 통해서 패턴에 매칭된 템플릿들을 파싱해준다.
이후, main
함수에 cache
를 초기화해주기 위한 코드를 넣어주고, 핸들러에 dependency injection을 하기위해 application struct에 다음과 같이 써주도록 하자.
func main() {
addr := flag.String("addr", ":4000", "HTTP network address")
dsn := flag.String("dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name")
flag.Parse()
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
db, err := openDB(*dsn)
if err != nil {
errorLog.Fatal(err)
}
defer db.Close()
templateCache, err := newTemplateCache("./ui/html/")
if err != nil {
errorLog.Fatal(err)
}
app := &application{
errorLog: errorLog,
infoLog: infoLog,
snippets: &mysql.SnippetModel{DB: db},
templateCache: templateCache,
}
srv := &http.Server{
Addr: *addr,
ErrorLog: errorLog,
Handler: app.routes(),
}
infoLog.Printf("Starting server on %s", *addr)
err = srv.ListenAndServe()
errorLog.Fatal(err)
}
templateCache, err := newTemplateCache("./ui/html/")
로 templateCache
인스턴스를 만들고 이를 application
인스턴스에 할당해주었다.
이제 우리는 우리의 페이지에 대한 template cache를 갖게 되었다. 이제 두번째 문제인 duplicated code 문제를 해결해보자 그리고 helper
메서드를 만들어 쉽게 cache로 부터 templates를 렌더링하도록 하자.
func (app *application) render(w http.ResponseWriter, r *http.Request, name string, td *templateData) {
// Retrieve the appropriate template set from the cache based on the page name
// (like 'home.page.tmpl'). If no entry exists in the cache with the
// provided name, call the serverError helper method that we made earlier.
ts, ok := app.templateCache[name]
if !ok {
app.serverError(w, fmt.Errorf("The template %s does not exist", name))
return
}
// Execute the template set, passing in any dynamic data.
err := ts.Execute(w, td)
if err != nil {
app.serverError(w, err)
}
}
현재 render
에는 http.Request
를 사용하는 부분이 없다. 이는 추후에 사용되기 때문에 따로 빼두었다.
이제 helpers의 render
메서드를 사용하여 handler
코드를 간결하게 만들어보자.
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
app.notFound(w)
return
}
s, err := app.snippets.Latest()
if err != nil {
app.serverError(w, err)
return
}
app.render(w, r, "home.page.tmpl", &templateData{
Snippets: s,
})
}
func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil || id < 1 {
app.notFound(w)
return
}
s, err := app.snippets.Get(id)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
app.notFound(w)
} else {
app.serverError(w, err)
}
return
}
app.render(w, r, "show.page.tmpl", &templateData{
Snippet: s,
})
}
렌더링하는 코드 부분을 helper로 두어서 훨씬 더 코드가 간결해졌다.
html templates에 dynamic behavior를 추가하는 것은 runtime error를 발생시키는 위험이 있다.
일부러 show.page.tmpl
template에 에러를 만들어보자, 그리고 무슨 일이 발생하는 지 알아보자.
{{template "base" .}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
{{with .Snippet}}
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
{{len nil}} <!-- Deliberate error -->
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
{{end}}
</div>
{{end}}
{{len nil}} <!-- Deliberate error -->
부분이 일부러 에러를 발생시킨 부분이다. nil
의 length를 가져오려고 하니 에러가 발생할 것이다.
이제 서버를 실행시키고 일부러 만든 에러를 확인해보자.
curl -i http://localhost:4000/snippet?id=1
HTTP/1.1 200 OK
Date: Mon, 26 Dec 2022 05:22:51 GMT
Content-Length: 741
Content-Type: text/html; charset=utf-8
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Snippet #1 - Snippetbox</title>
<link rel='stylesheet' href='/static/css/main.css'>
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<nav>
<a href='/'>Home</a>
</nav>
<main>
<div class='snippet'>
<div class='metadata'>
<strong>An old silent pond</strong>
<span>#1</span>
</div>
Internal Server Error
분명히 에러가 발생했지만 응답은 HTTP/1.1 200 OK
으로 나온다.
이는 굉장히 안좋은 상태이다. 제대로 된 html page도 안나오고 상태코드도 엉망이기 때문이다. 이러한 일이 발생하는 이유는 render
함수에서 제대로 코드를 작성하지 않았기 때문이다.
func (app *application) render(w http.ResponseWriter, r *http.Request, name string, td *templateData) {
ts, ok := app.templateCache[name]
if !ok {
app.serverError(w, fmt.Errorf("The template %s does not exist", name))
return
}
err := ts.Execute(w, td)
if err != nil {
app.serverError(w, err)
}
}
이미 w
에 값을 먼저 쓰기 때문에 헤더가 200 ok로 쓰이고, error가 발견되어 app.serverError
가 동작하면 Internal Server Error
가 추가되지만 header는 이미 쓰여있기 때문에 변경되지 않는다. 이러한 문제를 해결하기 위해서는 ts.Execute
메서드를 임시로 try해보고 결과를 반환받아 보는 것이 좋다.
func (app *application) render(w http.ResponseWriter, r *http.Request, name string, td *templateData) {
ts, ok := app.templateCache[name]
if !ok {
app.serverError(w, fmt.Errorf("The template %s does not exist", name))
return
}
// Initialize a new buffer.
buf := new(bytes.Buffer)
// Write the template to the buffer, instead of straight to the
// http.ResponseWriter. If there's an error, call our serverError helper and then
// return.
err := ts.Execute(buf, td)
if err != nil {
app.serverError(w, err)
return
}
// Write the contents of the buffer to the http.ResponseWriter. Again, this
// is another time where we pass our http.ResponseWriter to a function that
// takes an io.Writer.
buf.WriteTo(w)
}
다음과 같이 buf := new(bytes.Buffer)
를 먼저 생성하여 buf
를 가지고 err := ts.Execute(buf, td)
에 먼저 try해본다. 이후 결과에 따라 에러가 발생하면 app.serverError
를 넣어주고, error가 발생하지 않았다면 buf.WriteTo
로 http.ResponseWriter
에 값을 넘겨주면 된다.
다시 서버를 실행하여 결과를 확인해보도록 하자.
curl -i http://localhost:4000/snippet?id=1
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Mon, 26 Dec 2022 05:40:13 GMT
Content-Length: 22
Internal Server Error
재대로 된 결과가 나온 것을 확인할 수 있다.
이제 show.page.tmpl
에서 {{len nil}} <!-- Deliberate error -->
인 의도한 에러를 삭제해주도록 하자.
일부 web application에서 하나 이상 또는 심지어 모든 webpage에 포함하고 싶은 common dynamic data가 있을 수 있다. 가령 우리는 user의 name과 profile picture 또는 CSRF 토큰을 모든 페이지의 form형식으로 포함하고 싶을 수 있다.
우리는 간단하게, 현재 년도를 모든 page에 추가하고싶다고 하자.
이를 위해서 templateData
구조체에 CurrentYear
필드를 추가하도록 하자.
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
}
이제 다음 단계는 addDefaultData()
helper 메서드를 우리의 application에 추가할 차례이다. 이는 templateData
구조체의 인스턴스에 CurrentYear
를 inject하게 해줄 것이다.
그리고 나서 우리는 render()
helper 함수를 호출하여 default data를 각 페이지에 자동적으로 추가할 수 있도록 할 수 있다.
func (app *application) addDefaultData(td *templateData, r *http.Request) *templateData {
if td == nil {
td = &templateData{}
}
td.CurrentYear = time.Now().Year()
return td
}
func (app *application) render(w http.ResponseWriter, r *http.Request, name string, td *templateData) {
ts, ok := app.templateCache[name]
if !ok {
app.serverError(w, fmt.Errorf("The template %s does not exist", name))
return
}
buf := new(bytes.Buffer)
err := ts.Execute(buf, app.addDefaultData(td, r))
if err != nil {
app.serverError(w, err)
return
}
buf.WriteTo(w)
}
addDefaultData
는 templateData
를 포인터로 받아서 데이터가 없다면 만들어주고, 있다면 기존 데이터에 현재년도를 추가를 해주도록 한다.
해당 메서드를 render
메서드 안에 err := ts.Execute(buf, app.addDefaultData(td, r))
으로 추가해주면 된다.
이제 년도를 그려주기 위해서 footer.partial.tmpl
에 현재년도를 추가해주도록 하자.
{{define "footer"}}
<footer>Powered by <a href='https://golang.org/'>Go</a> in {{.CurrentYear}}</footer>
{{end}}
다시 서버를 시작하고 접속해보면, 하단 footer에 현재 년도가 있는 것을 확인할 수 있을 것이다.
go template에 사용하기위해 custom한 function을 만들 수 있다. 즉, .tmpl
파일에 {{define}}
과 같은 action을 만들 수 있다는 것이다.
humanDate()
함수를 만들어 datetime을 인간이 보기 좋은 형식으로 보이게 하자. 가령 2019-01-02 15:04:00 +0000 UTC
를 02 Jan 2019 at 15:04
와 같이 보이도록 하는 것이다.
이를 위해서 두 가지 주요한 과정이 있다.
humanData()
함수를 포함하는 template.FuncMap
객체를 만들도록 하자.template.Funcs()
메서드를 사용해야 한다.이를 template.go
파일에 적용시켜보자.
// Create a humanDate function which returns a nicely formatted string
// representation of a time.Time object.
func humanDate(t time.Time) string {
return t.Format("02 Jan 2006 at 15:04")
}
// Initialize a template.FuncMap object and store it in a global variable. This is
// essentially a string-keyed map which acts as a lookup between the names of our
// custom template functions and the functions themselves.
var functions = template.FuncMap{
"humanDate": humanDate,
}
func newTemplateCache(dir string) (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
pages, err := filepath.Glob(filepath.Join(dir, "*.page.tmpl"))
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
// The template.FuncMap must be registered with the template set before you
// call the ParseFiles() method. This means we have to use template.New() to
// create an empty template set, use the Funcs() method to register the
// template.FuncMap, and then parse the file as normal.
ts, err := template.New(name).Funcs(functions).ParseFiles(page)
if err != nil {
return nil, err
}
// ts, err = template.ParseFiles(page)
// if err != nil {
// return nil, err
// }
ts, err = ts.ParseGlob(filepath.Join(dir, "*.layout.tmpl"))
if err != nil {
return nil, err
}
ts, err = ts.ParseGlob(filepath.Join(dir, "*.partial.tmpl"))
if err != nil {
return nil, err
}
cache[name] = ts
}
return cache, nil
}
template.FuncMap
에 custom template function
을 string-function(key-value)로 넣어준다. 이를 통해서 humanData
라는 문자열로 humanDate
함수에 접근할 수 있게 되는 것이다.
마지막으로 ts, err := template.New(name).Funcs(functions).ParseFiles(page)
에 우리가 만든 template.FuncMap
을 넘겨주어야 하는데, 이는 template set에 우리가 만든 함수 mapping을 등록해주는 것이다. 이는 반드시 ParseFiles()
메서드 이전에 호출되어야 한다. 이는 template.New()
를 사용하기 위해서 empty template set
을 만들어야 한다는 것이며 Func()
메서드를 사용하여 template.FuncMap
을 등록한다음 file을 정상적으로 파싱하는 것이다.
humanData()
와 같은 custom template function
은 여러 개의 파라미터를 받을 수 있지만, 반드시 한 개의 값을 반환해야 한다. 단 한 가지의 예외 케이스가 있는데, 두 번째 값으로 error를 반환하는 경우말고는 없다.
이제 우리의 humanData()
함수를 build-in template function들과 같은 방법으로 사용해보도록 하자.
{{template "base" .}}
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
{{if .Snippets}}
<table>
<tr>
<th>Title</th>
<th>Created</th>
<th>ID</th>
</tr>
{{range .Snippets}}
<tr>
<td><a href='/snippet?id={{.ID}}'>{{.Title}}</a></td>
<td>{{humanDate .Created}}</td>
<td>#{{.ID}}</td>
</tr>
{{end}}
</table>
{{else}}
<p>There's nothing to see here... yet!</p>
{{end}}
{{end}}
<td>{{humanDate .Created}}</td>
을 넣으면 자동으로 template
에서 우리가 등록한 FuncMap
을 통해서 humanDate
의 함수를 찾아주어 실행해준다.
{{template "base" .}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
{{with .Snippet}}
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{humanDate .Created}}</time>
<time>Expires: {{humanDate .Expires}}</time>
</div>
{{end}}
</div>
{{end}}
show.page.tmpl
에도 우리가 만든 humanDate
를 사용해주고 렌더링을 해주면
go run ./...
24 Jan 2022 at 04:38
다음과 같이 사람이 알아보기 좋은 형식으로 값이 출력될 것이다.
추가적으로 재밌게도 template
에서는 pipeline
을 제공하는데 pipeline
은 unix like system에서 제공하는 |
가 맞다. 그래서 다음은 동일한 결과를 반환한다.
<time>Created: {{humanDate .Created}}</time>
<time>Created: {{.Created | humanDate}}</time>
이를 이용하여 다음과 같은 구문도 만들 수 있다.
<time>{{.Created | humanDate | printf "Created: %s"}}</time>