Go 실행 파일에 리소스 파일 포함시키기
Go 실행 파일에서 리소스 파일들을 포함시키는 방법을 알아본다.
필요성
팀 서버에 submit 된 PGP 파일을 decrypt 해 주는 서비스를 구현하였는데, 리소스 파일들을 파일 구조로 함께 배포할 수도 있지만 변경될 필요가 없는 리소스들은 편의상 실행 파일에 포함시켜서 배포하기로 하였다.
이렇게 하면 리소스 파일을 임의로 변경하는 것을 방지할 수 있고, 이후 서버 프로그램이나 리소스 파일들이 변경된 경우에도 실행 파일 한 개만 배포하면 되므로 아주 편리하다.
Go 언어는 특히 이런 경우에 유리한 점이 있는 것 같다.
기존 방법
Go 언어 자체적으로 embed 시켜주는 모듈이 없었으므로, 외부 패키지를 이용했어야 됐다.
이를 위한 여러 패키지들이 있었지만, 그 중에서 나는 주로 Go rice 패키지를 이용했었다. 아래는 간단한 사용 예이다.
- 아래와 같이 디렉구조와 파일을 생성하였다.
├─templates/index.html └─main.go
- 아래와 같이 실행하면
templates
디렉토리의 파일들의 내용을 포함하는rice-box.go
파일이 생성된다.$ rice embed-go
- 아래는 main.go 파일에서의 사용법 예이다.
package main import ( "html/template" "net/http" rice "github.com/GeertJohan/go.rice" "github.com/gin-gonic/contrib/renders/multitemplate" "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" ) func main() { router := gin.New() // Home 핸들러 router.GET("/", homeHandler) // 템플릿 html 파일들을 로딩 router.HTMLRender = loadTemplates() router.Run(":8080") } // 템플릿 html 파일들을 embedding해서 로딩한다. func loadTemplates() multitemplate.Render { r := multitemplate.New() templateBox, err := rice.FindBox("templates") if err != nil { return r } list := [...]string{"index.html"} for _, x := range list { templateString, err := templateBox.String(x) if err != nil { continue } tmplMessage, err := template.New(x).Parse(templateString) if err != nil { continue } r.Add(x, tmplMessage) } return r } func homeHandler(ctx *gin.Context) { ctx.HTML(http.StatusOK, "index.html", nil) }
그런데 이번에 PGP 파일을 decrypt 해 주는 서비스를 구현할 때에도 기존 rice 패키지를 이용하는 방법을 이용하여 금방 구현은 했는데, 기존에 없던 favicon
을 이 방법으로 포함시켜려고 하니깐 의외로 잘 되지 않았다.
그래서 다른 방법은 없을까 하고 구글링 해보니, 최근에 나온 Go 버전 1.16 부터는 embed
패키지가 기본 내장되었고 이를 이용하여 쉽게 구현할 수 있을 것 같아서 시도해 보았다. 🤔
embed 모듈을 이용한 방법
앞서 언급했듯이 Go 버전 1.16부터는 자체적으로 embed 모듈을 지원하므로, 외부 패키지를 이용하지 않아도 된다. Go embed 패키지 페이지에 기본적인 사용법이 나와 있다.
Gin에 적용 예제
- 아래와 같이 디렉구조와 파일을 생성하였다.
├─static │ ├─css/style.css │ ├─icon/favicon.png │ └─template/index.html └─main.go
style.css
파일의 내용은 아래와 같이 작성하였다.body { background-color: lightgray; }
- 아래와 같이
main.go
파일을 작성하였다.package main import ( "embed" "fmt" "html/template" "net/http" "github.com/gin-gonic/gin" ) // "static" 디렉토리 embed var ( //go:embed static embedFS embed.FS ) func main() { router := gin.Default() // HTML 템플릿 파일들을 로드해서 세팅한다. template, err := template.ParseFS(embedFS, "static/template/*") if err != nil { fmt.Println("Fail to load template.", err) return } router.SetHTMLTemplate(template) // Home 핸들러 router.GET("/", homeHandler) // Static embedded 파일들을 serve router.GET("/static/*filepath", func(c *gin.Context) { c.FileFromFS(c.Request.URL.Path, http.FS(embedFS)) }) router.Run() } func homeHandler(ctx *gin.Context) { ctx.HTML(http.StatusOK, "index.html", nil) }
위의 코드에서처럼 아래와 같이
//go:embed 디렉토리
내용을 추가해 주면 이 디렉토리의 모든 파일들이 포함된다.//go:embed static embedFS embed.FS
이후
embed.FS
를 이용하여 SetHTMLTemplate() 함수를 수행해 주고, 아래와 같이 embedded 파일들을 serve 해 주면, 이후부터는 embedded 된 파일들을 바로 엑세스할 수 있게 된다.router.GET("/static/*filepath", func(c *gin.Context) { c.FileFromFS(c.Request.URL.Path, http.FS(embedFS)) })
index.html
파일의 내용은 아래와 같이 작성하였다.<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Go gin embed test</title> <link rel="stylesheet" href="/static/css/style.css"> <link rel="icon" href="/static/icon/favicon.png" type="image/png"> </head> <body> <h1>Hello! Go gin embed.</h1> </body> </html>
HTML 템플릿 파일에서도 위의 예에서 보듯이
/static/
와 같이 리소스 파일들의 경로를 디렉토리와 파일 경로로 엑세스할 수 있게 된다. (위의 예에서는 style.css, favicon.png 파일 사용)
빌드하면 결과로 실행 파일 한 개만 나오고, 이 파일을 실행한 후에 웹 페이지에 접속해 보면 index.html, style.css, favicon.png 파일들이 내용이 정상적으로 반영되어서 나옴을 확인하였다.
Go 자체적으로 지원하는 embed 패키지를 사용하니 확실히 rice 패키지를 사용할 때마다 편리해졌다. 🍻