Go 1.16 embed 패키지로 정적 파일 바이너리에 포함하기

배경

사내 CLI 도구를 Go로 만들면서 템플릿 파일들을 함께 배포해야 하는 상황이었다. 기존에는 go-bindata를 사용했는데, 빌드 파이프라인에 별도 단계를 추가해야 해서 번거로웠다.

Go 1.16에서 embed 패키지가 표준으로 추가되면서 이 문제가 깔끔하게 해결됐다.

기본 사용법

단일 파일을 임베드하는 경우:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed template.txt
var templateContent string

func main() {
    fmt.Println(templateContent)
}

주석과 변수 선언 사이에 다른 코드가 들어가면 안 된다는 점을 주의해야 한다.

여러 파일 임베드

디렉토리 전체를 임베드하려면 embed.FS를 사용한다:

package main

import (
    "embed"
    "io/fs"
    "log"
)

//go:embed templates/*
var templatesFS embed.FS

func main() {
    data, err := templatesFS.ReadFile("templates/config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    
    // 또는 fs.FS 인터페이스로 사용
    fs.WalkDir(templatesFS, "templates", func(path string, d fs.DirEntry, err error) error {
        if !d.IsDir() {
            log.Println(path)
        }
        return nil
    })
}

실제 적용 사례

CLI 도구에서 프로젝트 초기화 템플릿을 제공하는 코드:

//go:embed scaffolds/*
var scaffolds embed.FS

func InitProject(projectType string) error {
    dir := fmt.Sprintf("scaffolds/%s", projectType)
    
    return fs.WalkDir(scaffolds, dir, func(path string, d fs.DirEntry, err error) error {
        if err != nil || d.IsDir() {
            return err
        }
        
        content, err := scaffolds.ReadFile(path)
        if err != nil {
            return err
        }
        
        targetPath := strings.TrimPrefix(path, dir+"/")
        return os.WriteFile(targetPath, content, 0644)
    })
}

장점

  • 서드파티 도구 불필요
  • 빌드 파이프라인 단순화
  • 단일 바이너리 배포 가능
  • fs.FS 인터페이스 호환으로 기존 코드와 통합 용이

Go 1.16 이상을 사용하는 프로젝트라면 적극 활용할 만하다.

Go 1.16 embed 패키지로 정적 파일 바이너리에 포함하기