Ver2.5 Go 언어로 만드는 사용자 친화적 CLI 도구: 터미널을 내 손안의 놀이터로!

Go 언어로 만드는 사용자 친화적 CLI 도구: 터미널을 내 손안의 놀이터로!
딱딱한 검은 화면은 이제 그만! Go와 함께라면 당신의 CLI도 힙스터가 될 수 있어요. 😎
들어가며: 왜 우리는 다시 CLI에 주목해야 할까?
안녕! 👋 오늘 우리가 함께 떠날 여정은 바로 'Go 언어로 만드는 사용자 친화적 CLI(Command Line Interface) 도구'의 세계야.
"아니, 요즘 세상에 누가 까만 화면에 글자만 치고 있어?" 라고 생각할 수도 있겠지. 맞아, 화려한 GUI(Graphical User Interface)가 세상을 지배하는 것처럼 보이니까. 하지만 개발자들의 세계, 특히 클라우드, DevOps, 시스템 자동화 영역에서는 CLI가 여전히, 아니 오히려 *더욱* 강력한 힘을 발휘하고 있어.
생각해봐. 마우스로 수십 번 클릭해야 하는 작업을 단 한 줄의 명령어로 끝낼 수 있다면?
수백 대의 서버에 동일한 설정을 적용해야 할 때, GUI로는 상상도 못 할 일을 CLI 스크립트 하나로 해결할 수 있다면?
이게 바로 CLI의 '짬에서 나오는 바이브'랄까. 효율성, 자동화, 그리고 개발자 경험(DX, Developer Experience)의 끝판왕이지.
특히 Go 언어는 이런 CLI 툴을 만들기에 최적화된 언어야. 왜냐고? 이유는 차고 넘치지!
1. 미친 컴파일 속도와 단일 바이너리: Go는 컴파일이 엄청나게 빨라. `go build`를 치는 순간 거의 바로 실행 파일이 뚝딱! 하고 만들어져. 게다가 이 실행 파일은 다른 라이브러리나 런타임 설치 없이, 그냥 파일 하나만 툭 던져주면 어디서든 실행돼. 윈도우, 맥, 리눅스? 문제없어. 이걸 '의존성 지옥'에서 벗어났다고 표현해. 👼
2. 동시성(Concurrency) 내장: Go는 '고루틴(Goroutine)'이라는 아주 가볍고 강력한 동시성 모델을 언어 차원에서 지원해. 여러 작업을 동시에 처리해야 하는 CLI 툴(예: 여러 서버에 동시에 접속해서 정보 가져오기)을 만들 때, 복잡한 스레드 관리 없이 너무나도 쉽게 구현할 수 있어.
3. 강력한 표준 라이브러리: 네트워크, 파일 시스템, 입출력 등 CLI 개발에 필요한 대부분의 기능이 Go 표준 라이브러리에 이미 다 들어있어. 외부 라이브러리를 덕지덕지 붙이지 않아도 꽤 괜찮은 툴을 만들 수 있다는 거지.
4. 성장하는 생태계: 전 세계의 수많은 개발자들이 Go로 CLI 툴을 만들고 있어. 쿠버네티스(kubectl), 도커(docker), 테라폼(terraform) 같은 DevOps 분야의 거물들이 모두 Go로 만들어졌다는 사실! 이건 곧 우리가 참고할 수 있는 훌륭한 라이브러리와 예제가 넘쳐난다는 뜻이야.
오늘 우리는 단순히 "작동하는" CLI를 넘어, "사용하고 싶은" CLI를 만드는 법을 배울 거야.
명령어 자동완성, 예쁜 색상 출력, 진행 상태를 보여주는 프로그레스 바, 사용자에게 질문을 던지는 인터랙티브 프롬프트까지.
터미널을 더 이상 두려움의 대상이 아닌, 내 손안의 강력하고 편리한 놀이터로 만들어 보자고! 🚀
이 글은 누구를 위한 건가요?
이 글은 Go 언어의 기본 문법은 알고 있지만, 이를 활용해 실용적인 애플리케이션을 만들어보고 싶은 사람들을 위해 쓰였어. 터미널 환경에 익숙하지만, 매번 만들던 `hello world` 수준의 CLI가 지겨웠던 개발자라면 더욱 환영이야! 당신의 개발 실력과 포트폴리오를 한 단계 업그레이드할 수 있는 좋은 기회가 될 거야.
1단계: 기본 다지기 - Go 표준 라이브러리 `flag` 패키지 정복
모든 위대한 여정은 첫걸음부터 시작되지. Go CLI 개발의 첫걸음은 바로 표준 라이브러리에 내장된 `flag` 패키지야.
외부 라이브러리 없이, Go만 설치되어 있다면 바로 사용할 수 있는 가장 기본적이고 중요한 녀석이지.
`flag` 패키지는 프로그램 실행 시 전달되는 커맨드 라인 인자(argument)를 파싱(parsing)하는 역할을 해.
예를 들어, `my-app -name="John" -age=30` 이런 명령어를 실행할 때, `-name`이나 `-age` 같은 부분을 '플래그(flag)'라고 불러. `flag` 패키지는 이런 플래그들을 손쉽게 정의하고, 사용자가 입력한 값을 우리 프로그램의 변수로 쏙쏙 가져와 주는 마법을 부리지.
가장 심플한 예제: 문자열 플래그 받아보기
백문이 불여일견! 바로 코드를 보자. 특정 사람에게 인사하는 간단한 프로그램을 만들어 볼 거야.
package main
import (
"flag"
"fmt"
)
func main() {
// 1. 플래그를 정의합니다.
// flag.String("플래그이름", "기본값", "설명")
// 이 함수는 해당 플래그의 값을 담을 *string 포인터를 반환합니다.
namePtr := flag.String("name", "World", "a name to say hello to")
// 2. 커맨드 라인 인자를 파싱합니다.
// 이 함수가 호출되어야 위에서 정의한 플래그들이 실제 값으로 채워집니다.
flag.Parse()
// 3. 파싱된 값을 사용합니다.
// 포인터이므로, 실제 값에 접근하려면 역참조(*)를 사용해야 합니다.
fmt.Printf("Hello, %s!\n", *namePtr)
}
자, 코드는 이게 전부야. 정말 간단하지? 이제 이 코드를 실행해 보자.
먼저 빌드를 하고:
$ go build
이제 몇 가지 방법으로 실행해 볼까?
1. 아무 플래그도 주지 않았을 때:
$ ./your-app-name
Hello, World!
우리가 `flag.String`의 두 번째 인자로 넘겨준 기본값 "World"가 사용된 걸 볼 수 있어.
2. `-name` 플래그를 사용했을 때:
$ ./your-app-name -name="Gopher"
Hello, Gopher!
짜잔! 이제 "Gopher"에게 인사하네. `-name "Gopher"`나 `-name Gopher` 형식도 모두 지원돼. 유연하지?
3. `-h` 또는 `--help` 플래그를 사용했을 때:
$ ./your-app-name -h
Usage of ./your-app-name:
-name string
a name to say hello to (default "World")
이게 바로 `flag` 패키지의 킬링 포인트 중 하나야. 우리는 도움말을 따로 만든 적이 없는데, `flag` 패키지가 알아서 `-h`나 `--help` 플래그를 처리하고, 우리가 플래그를 정의할 때 세 번째 인자로 넘겨준 "설명"을 이용해 멋진 사용법 안내를 만들어줘. 이것이야말로 사용자 친화적 CLI의 첫걸음!
다양한 타입의 플래그 지원
`flag` 패키지는 문자열(`String`) 뿐만 아니라 다양한 타입을 지원해. 정말 편리하지.
- `flag.Int()`: 정수 타입 플래그
- `flag.Bool()`: 불리언(true/false) 타입 플래그
- `flag.Float64()`: 부동소수점 타입 플래그
- `flag.Duration()`: 시간 간격 타입 플래그 (예: "1h30m")
이번엔 여러 타입을 한번에 사용하는 예제를 만들어보자. 커피 주문 프로그램이야. ☕
package main
import (
"flag"
"fmt"
"time"
)
func main() {
// 다양한 타입의 플래그 정의
coffeeType := flag.String("type", "Americano", "Type of coffee")
quantity := flag.Int("qty", 1, "Number of coffees")
isIced := flag.Bool("iced", false, "Is it an iced coffee?")
waitTime := flag.Duration("wait", 5*time.Minute, "Time to wait for the coffee")
flag.Parse()
fmt.Printf("--- 주문 내역 ---\n")
fmt.Printf("커피 종류: %s\n", *coffeeType)
fmt.Printf("수량: %d잔\n", *quantity)
if *isIced {
fmt.Println("옵션: 아이스 ❄️")
} else {
fmt.Println("옵션: 핫 🔥")
}
fmt.Printf("예상 대기 시간: %v\n", *waitTime)
fmt.Println("------------------")
}
이 프로그램을 빌드하고 실행해 보자.
$ go build -o coffee-order
$ ./coffee-order -type="Latte" -qty=2 -iced -wait=10m
--- 주문 내역 ---
커피 종류: Latte
수량: 2잔
옵션: 아이스 ❄️
예상 대기 시간: 10m0s
------------------
주목할 점은 `-iced` 부분이야. `bool` 타입 플래그는 값을 명시하지 않고 플래그 이름만 적어주면 `true`로 인식돼. (`-iced=true`와 동일한 효과) 만약 `false`로 하고 싶다면 `-iced=false`라고 명시적으로 적어줘야 해. 기본값은 `false`이므로, 핫 커피를 원하면 그냥 이 플래그를 생략하면 되겠지.
포인터가 싫다면? `flag.Var`를 사용하자!
`flag.String`, `flag.Int` 등은 모두 해당 타입의 포인터를 반환해. 그래서 값을 사용할 때마다 `*`를 붙여줘야 해서 조금 귀찮을 수 있어. 이럴 땐 `flag.StringVar`, `flag.IntVar` 같은 함수를 사용하면 돼. 이 함수들은 변수의 주소를 직접 받아서 파싱된 값을 채워 넣어줘.
// 이전 코드
// namePtr := flag.String("name", "World", "description")
// fmt.Println(*namePtr)
// Var 함수 사용
var name string
flag.StringVar(&name, "name", "World", "description")
flag.Parse()
fmt.Println(name) // 이제 *가 필요 없어! 훨씬 깔끔하지?
플래그가 아닌 인자(Non-flag arguments) 처리하기
모든 인자가 `-`로 시작하는 플래그는 아니야. `git clone https://...` 에서 `clone`이나 `https://...` 같은 것들은 플래그가 아닌 일반 인자지. `flag` 패키지는 이런 인자들도 처리할 수 있어.
`flag.Parse()`가 호출되고 나면, 플래그가 아닌 나머지 인자들은 `flag.Args()` 슬라이스에 순서대로 저장돼.
package main
import (
"flag"
"fmt"
"strings"
)
func main() {
// -s 플래그는 구분자(separator)를 지정하는 데 사용
separator := flag.String("s", " ", "separator character")
flag.Parse()
// flag.Args()는 플래그가 아닌 나머지 인자들의 슬라이스를 반환
args := flag.Args()
if len(args) == 0 {
fmt.Println("Please provide some text to join.")
return
}
// 모든 인자들을 우리가 지정한 구분자로 합쳐서 출력
result := strings.Join(args, *separator)
fmt.Println(result)
}
실행 결과는 어떨까?
$ go build -o joiner
$ ./joiner hello world this is go
hello world this is go
$ ./joiner -s="," hello world this is go
hello,world,this,is,go
$ ./joiner -s="---" a b c d e
a---b---c---d---e
보는 것처럼 `-s` 플래그는 `separator` 변수에 잘 담기고, 나머지 "hello", "world", ... 같은 단어들은 `flag.Args()`를 통해 얻은 슬라이스에 담겨서 `strings.Join` 함수의 재료로 쓰였어. 이 기능을 이용하면 `cp source.txt target.txt` 처럼 여러 개의 파일 경로를 받는 프로그램도 쉽게 만들 수 있겠지?
`flag` 패키지는 Go CLI 개발의 훌륭한 출발점이야. 하지만 프로그램이 복잡해지기 시작하면 한계가 드러나. 예를 들어, `git commit -m "message"` 처럼 `commit`이라는 '서브커맨드(subcommand)'가 있고, 그 서브커맨드에만 적용되는 `-m` 플래그가 있는 구조를 `flag` 패키지만으로 만들려면 꽤나 머리가 아파져. 🤯
그래서 다음 단계에서는 이 모든 걸 우아하게 해결해 줄 CLI 라이브러리의 끝판왕, 'Cobra'를 만나러 가볼 거야. 준비됐어? Let's Go!
2단계: 날개를 달아보자 - CLI 프레임워크의 제왕, Cobra 🐍
자, `flag` 패키지로 기본기를 다졌다면 이제 본격적으로 하늘을 날아볼 시간이야. 우리가 탈 비행기의 이름은 바로 Cobra! 이름처럼 강력하고, 한 번 맛 들이면 헤어 나올 수 없는 매력을 가진 녀석이지. Cobra는 Go CLI 애플리케이션을 위한 가장 인기 있고 강력한 프레임워크 중 하나야. 앞서 말했던 `kubectl`, `hugo`, `gh` (GitHub CLI) 등 수많은 유명 프로젝트들이 Cobra를 기반으로 만들어졌어.
Cobra가 왜 그렇게 대단하냐고? Cobra는 '커맨드(Command)' 기반의 구조를 아주 쉽게 만들어줘.
위 그림처럼, 우리 앱(`my-app`)이라는 최상위 '루트 커맨드(Root Command)'가 있고, 그 아래에 `server`, `user`, `version` 같은 '서브 커맨드(Sub Command)'를 가질 수 있어. 심지어 `user` 아래에 `create`라는 또 다른 서브 커맨드를 붙여 `my-app user create` 같은 구조도 만들 수 있지. 이걸 '커맨드 중첩(Nesting)'이라고 해.
각 커맨드는 자신만의 플래그를 가질 수 있어. `server` 커맨드는 `--port` 플래그를, `user` 커맨드는 `--name` 플래그를 갖는 식이지. 루트 커맨드에 정의된 `--verbose` 같은 플래그는 모든 서브 커맨드에서 공통으로 사용할 수도 있어. 이런 걸 '전역 플래그(Persistent Flag)'라고 불러.
이런 복잡한 구조를 `flag` 패키지만으로 구현하려면 조건문 지옥에 빠지겠지만, Cobra와 함께라면 아주 직관적이고 깔끔하게 코드를 구성할 수 있어. 자, 이제 직접 Cobra 프로젝트를 시작해 보자!
Cobra-CLI로 프로젝트 구조 자동 생성하기
Cobra는 자체적으로 프로젝트의 뼈대를 만들어주는 CLI 툴(`cobra-cli`)을 제공해. 이걸 쓰면 정말 편해. 먼저 `cobra-cli`를 설치하자.
$ go install github.com/spf13/cobra-cli@latest
설치가 끝났다면, 이제 우리 프로젝트를 초기화할 차례야. `my-cli`라는 이름의 프로젝트를 만들어 볼게.
$ mkdir my-cli
$ cd my-cli
$ cobra-cli init
이 명령을 실행하면, 현재 디렉토리에 몇 개의 파일과 디렉토리가 마법처럼 생겨날 거야.
my-cli/
├── cmd/
│ └── root.go
├── main.go
└── LICENSE
- main.go: 프로그램의 진입점이야. 아주 심플하게 `cmd.Execute()` 함수만 호출하는 역할을 해.
- cmd/root.go: 여기가 바로 루트 커맨드(`my-cli` 자체)가 정의되는 곳이야. 우리 앱의 심장부라고 할 수 있지.
이제 `main.go`를 열어보면 내용은 아마 이럴 거야.
package main
import "my-cli/cmd"
func main() {
cmd.Execute()
}
그리고 `cmd/root.go`는 Cobra 커맨드의 기본 구조를 보여줘.
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "my-cli",
Short: "A brief description of your application",
Long: `A longer description that can span multiple lines
and contain examples and usage of using your application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
// 여기에 플래그 정의 등을 할 수 있습니다.
}
코드를 보면 `cobra.Command` 구조체가 핵심인 걸 알 수 있어. 몇 가지 중요한 필드를 살펴보자.
- `Use`: 커맨드의 이름이야. 사용법에 표시되지.
- `Short`: 짧은 한 줄 설명. `my-cli --help`를 쳤을 때 목록에 보여.
- `Long`: 긴 설명. `my-cli help`나 `my-cli [command] --help`를 쳤을 때 보여. 마크다운처럼 여러 줄로 작성할 수 있어.
- `Run`: 이 커맨드가 실제로 실행될 때 수행할 로직을 담는 함수야. 지금은 주석 처리되어 있지만, 곧 여기에 우리의 코드를 넣을 거야.
첫 번째 서브 커맨드 추가하기
이제 `cobra-cli`를 이용해서 `hello`라는 서브 커맨드를 추가해 보자. 터미널에 이렇게 입력해봐.
$ cobra-cli add hello
그러면 `cmd/hello.go` 파일이 새로 생겼을 거야. 그리고 `root.go`의 `init` 함수에 `rootCmd.AddCommand(helloCmd)` 라는 코드가 자동으로 추가돼서 `hello` 커맨드를 루트 커맨드의 자식으로 등록해줘. 정말 똑똑하지? 👍
이제 `cmd/hello.go` 파일을 열어서 우리의 로직을 추가해 보자.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// helloCmd represents the hello command
var helloCmd = &cobra.Command{
Use: "hello [name]",
Short: "Prints a friendly greeting",
Long: `Prints a greeting to the specified name.
If no name is provided, it defaults to "World".`,
// Args 필드는 이 커맨드가 받을 인자의 개수를 검증해줘.
// cobra.MaximumNArgs(1)은 인자를 0개 또는 1개만 받을 수 있다는 뜻.
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var name string
if len(args) > 0 {
name = args[0]
} else {
name = "World"
}
fmt.Printf("Hello, %s!\n", name)
},
}
func init() {
rootCmd.AddCommand(helloCmd)
}
`Run` 함수 안에 실제 로직을 넣었어. `args` 슬라이스에 플래그가 아닌 인자들이 담겨 오는데, 우리는 그 첫 번째 값을 이름으로 사용하기로 했지. `Args: cobra.MaximumNArgs(1)` 이 부분도 중요해. 만약 사용자가 `my-cli hello John Doe` 처럼 인자를 2개 이상 주면, Cobra가 알아서 에러 메시지를 보여주고 프로그램을 종료시켜. 이런 유효성 검사를 직접 만들려면 얼마나 귀찮겠어?
이제 실행해볼까?
$ go run main.go hello
Hello, World!
$ go run main.go hello "Code Crafter"
Hello, Code Crafter!
$ go run main.go hello John Doe
Error: accepts at most 1 arg(s), received 2
완벽하게 작동해! 이제 도움말도 확인해보자.
$ go run main.go --help
A longer description that can span multiple lines...
Usage:
my-cli [command]
Available Commands:
completion generate the autocompletion script for the specified shell
hello Prints a friendly greeting
help Help about any command
Flags:
-h, --help help for my-cli
Use "my-cli [command] --help" for more information about a command.
우리가 만든 `hello` 커맨드가 `Available Commands` 목록에 예쁘게 들어가 있는 걸 볼 수 있어. `Short` 설명이 여기에 쓰이는 거지.
플래그 추가하기: 로컬 플래그와 전역 플래그
이제 `hello` 커맨드에 플래그를 추가해서 기능을 확장해 보자. 인사를 대문자로 외치게 하는 `--shout` 플래그를 추가해 볼게.
플래그는 `init` 함수에서 정의하는 게 일반적이야. Cobra는 `pflag`라는 라이브러리를 사용하는데, 이건 Go의 표준 `flag` 패키지를 개선한 버전이라 사용법이 거의 똑같아.
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var shout bool // 플래그 값을 담을 변수
var helloCmd = &cobra.Command{
Use: "hello [name]",
Short: "Prints a friendly greeting",
Long: `...`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var name string
if len(args) > 0 {
name = args[0]
} else {
name = "World"
}
greeting := fmt.Sprintf("Hello, %s!", name)
if shout {
greeting = strings.ToUpper(greeting)
}
fmt.Println(greeting)
},
}
func init() {
rootCmd.AddCommand(helloCmd)
// helloCmd에만 속하는 로컬 플래그를 추가합니다.
// Flags()는 로컬 플래그 셋을 반환합니다.
helloCmd.Flags().BoolVarP(&shout, "shout", "s", false, "Shout the greeting")
}
여기서 주목할 부분은 `helloCmd.Flags().BoolVarP(...)` 야.
- `Flags()`: 이 커맨드에만 적용되는 '로컬 플래그'를 추가할 때 사용해.
- `BoolVarP`: `pflag`의 함수인데, `P`가 붙은 함수는 '축약형(shorthand)'을 함께 지정할 수 있게 해줘. 즉, `--shout` 대신 `-s`를 쓸 수 있게 되는 거지. 너무 편리해!
- 인자는 순서대로 (값을 담을 변수의 포인터, 긴 이름, 짧은 이름, 기본값, 설명) 이야.
실행 결과:
$ go run main.go hello Gopher
Hello, Gopher!
$ go run main.go hello Gopher --shout
HELLO, GOPHER!
$ go run main.go hello Gopher -s
HELLO, GOPHER!
잘 작동하지? 그럼 모든 커맨드에서 사용할 수 있는 '전역 플래그'는 어떻게 만들까? 예를 들어, 디버그 로그를 출력하는 `--debug` 플래그를 추가하고 싶다고 해보자. 이건 `root.go`에서 정의하면 돼.
// ... (기존 코드)
var debug bool // 전역 플래그 값을 담을 변수
// ... (rootCmd 정의)
func init() {
// PersistentFlags()는 전역 플래그 셋을 반환합니다.
// 이 플래그는 rootCmd와 그 모든 자식 커맨드에서 사용할 수 있습니다.
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug mode")
}
`Flags()` 대신 `PersistentFlags()`를 사용한 것만 빼면 똑같아. 이제 `my-cli --debug`, `my-cli hello --debug` 처럼 모든 커맨드에서 `--debug` 플래그를 사용할 수 있게 됐어. `hello.go`의 `Run` 함수 안에서 `debug` 변수를 체크해서 추가 정보를 출력하는 로직을 넣으면 되겠지.
변수 스코프에 주의하세요! 플래그 값을 담는 변수(위 예제의 `shout`, `debug`)는 보통 커맨드 파일 내의 전역 변수로 선언돼. 이건 간단한 경우에는 괜찮지만, 애플리케이션이 커지면 관리하기 어려워질 수 있어. 더 좋은 방법은 `Run` 함수가 아닌 `RunE` 함수를 사용하고, `cmd.Flags().GetBool("shout")` 처럼 플래그 값을 직접 가져와서 사용하는 거야. `RunE`는 `error`를 반환할 수 있어서 에러 처리에도 더 용이해. 하지만 지금은 개념 이해를 위해 간단한 방식을 사용했어!
Cobra는 이 외에도 자동 완성 스크립트 생성, 라이선스 정보 추가, 커맨드 별 라이프사이클 훅(PersistentPreRun, PostRun 등) 등 정말 막강한 기능들을 제공해. Cobra만 잘 다뤄도 당신의 CLI는 이미 프로페셔널의 반열에 오른 거나 다름없어. 하지만... 아직 뭔가 부족하지 않아? 맞아. 너무 밋밋해. 다음 장에서는 우리의 CLI에 색을 입히고, 생명을 불어넣는 방법을 알아볼 거야. 🎨
3단계: 터미널에 감성을 더하다 - CLI의 UI/UX 레벨업!
자, 이제 우리 CLI는 튼튼한 뼈대(Cobra)를 갖췄어. 하지만 아직 영혼이 없어. 삭막한 흑백 텍스트만으로는 사용자의 마음을 사로잡을 수 없지. 이번 챕터에서는 CLI의 사용자 경험(UX)을 극적으로 향상시키는 다양한 기법과 라이브러리들을 만나볼 거야. 터미널을 단순한 명령어 실행기가 아닌, 사용자와 소통하는 인터랙티브한 공간으로 만들어 보자!
🎨 1. 텍스트에 색을 입히자: `fatih/color`
가장 쉽고 빠르게 시각적 변화를 줄 수 있는 방법은 바로 '색상'이야. 중요한 정보는 초록색으로, 경고는 노란색으로, 에러는 빨간색으로 보여준다면 사용자는 훨씬 빠르게 상황을 인지할 수 있어. Go에서는 `fatih/color` 라이브러리가 이 분야의 국민템이야.
먼저 라이브러리를 설치하자.
$ go get github.com/fatih/color
사용법은 너무나도 직관적이야.
package main
import "github.com/fatih/color"
func main() {
// 가장 기본적인 사용법
color.Cyan("이건 청록색 텍스트입니다.")
color.Blue("이건 파란색 텍스트입니다.")
color.Yellow("경고: 설정 파일이 없습니다.")
color.Red("에러: 데이터베이스 연결 실패!")
color.Green("성공: 작업이 완료되었습니다.")
// 여러 속성을 조합할 수도 있습니다.
c := color.New(color.FgCyan, color.Bold, color.Underline)
c.Println("청록색 + 볼드 + 밑줄 텍스트!")
// Printf 스타일의 포매팅도 물론 가능합니다.
color.Magenta("숫자: %d, 문자열: %s", 123, "hello")
}
이걸 우리 `hello` 커맨드에 적용해 볼까? 성공 메시지는 초록색으로 출력하도록 바꿔보자.
// ... (import 구문에 "github.com/fatih/color" 추가)
// ... (Run 함수 내부)
Run: func(cmd *cobra.Command, args []string) {
// ... (기존 로직)
greeting := fmt.Sprintf("Hello, %s!", name)
if shout {
greeting = strings.ToUpper(greeting)
}
// fmt.Println(greeting) 대신 color 라이브러리 사용
color.Green(greeting)
},
이제 `go run main.go hello`를 실행하면 초록색으로 인사가 나올 거야. 훨씬 보기 좋지? 단순한 색상 추가만으로도 앱의 완성도가 확 올라가는 느낌을 받을 수 있어.
⏳ 2. 기다림을 지루하지 않게: 스피너와 프로그레스 바
CLI 작업 중에는 시간이 걸리는 일들이 많아. 파일 다운로드, 데이터 처리, 원격 서버 접속 등... 이럴 때 아무런 피드백 없이 커서만 껌뻑이고 있으면 사용자는 '이거 멈춘 거 아니야?' 하고 불안해하게 돼. 이럴 때 필요한 게 바로 '나 지금 열심히 일하고 있어!'라고 알려주는 시각적 장치들이야.
스피너(Spinner)로 간단한 대기 표시: `briandowns/spinner`
작업 시간이 얼마나 걸릴지 정확히 모를 때, 스피너는 아주 유용한 도구야. 빙글빙글 돌아가는 애니메이션을 보여주면서 작업이 진행 중임을 알려주지.
$ go get github.com/briandowns/spinner
package main
import (
"time"
"github.com/briandowns/spinner"
)
func main() {
// 1. 스피너 생성
// 첫 번째 인자는 스피너 모양 세트, 두 번째 인자는 회전 속도
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
// 2. 스피너 메시지 설정 (선택 사항)
s.Suffix = " 오래 걸리는 작업 처리 중..."
// 3. 스피너 시작
s.Start()
// 4. 시간이 걸리는 작업을 시뮬레이션
time.Sleep(4 * time.Second)
// 5. 작업 완료 후 스피너 중지
s.Stop()
}
이 코드를 실행하면 4초 동안 터미널에서 예쁜 스피너가 돌아가다가 멈출 거야. `spinner.CharSets`에는 수십 가지의 다양한 스피너 모양이 미리 정의되어 있으니, 취향에 맞게 골라 쓰면 돼.
프로그레스 바(Progress Bar)로 진행률 표시: `schollz/progressbar`
전체 작업량을 알고 있고, 현재 얼마나 진행되었는지 보여줄 수 있다면 프로그레스 바가 최고의 선택이야. 파일 다운로드나 대용량 데이터 처리 시각화에 딱이지.
$ go get github.com/schollz/progressbar/v3
package main
import (
"time"
"github.com/schollz/progressbar/v3"
)
func main() {
// 1. 프로그레스 바 생성
// 첫 번째 인자는 전체 작업량(예: 100)
bar := progressbar.NewOptions(100,
progressbar.OptionSetDescription("파일 처리 중..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
)
// 2. 루프를 돌면서 작업 진행률 업데이트
for i := 0; i < 100; i++ {
bar.Add(1) // 진행률 1 증가
time.Sleep(40 * time.Millisecond)
}
}
이 코드는 `[===> ] 3%` 와 같은 익숙한 프로그레스 바를 터미널에 그려줘. `progressbar.NewOptions`를 사용하면 바의 모양, 설명, 색상 등 다양한 커스터마이징이 가능해. 사용자가 진행 상황을 눈으로 직접 확인할 수 있으니 훨씬 안정감을 느끼겠지?
🤔 3. 사용자에게 질문하기: 인터랙티브 프롬프트 `manifoldco/promptui`
때로는 사용자에게 직접 정보를 입력받거나, 여러 옵션 중 하나를 선택하게 해야 할 때가 있어. 예를 들어 "정말로 삭제하시겠습니까? (y/n)" 같은 확인 절차 말이야. 이럴 때 `manifoldco/promptui` 라이브러리를 사용하면 아주 세련된 인터랙티브 프롬프트를 만들 수 있어.
$ go get github.com/manifoldco/promptui
이 라이브러리는 크게 세 가지 기능을 제공해.
1. Prompt: 간단한 텍스트 입력받기
// ...
import "github.com/manifoldco/promptui"
func main() {
prompt := promptui.Prompt{
Label: "당신의 이름은 무엇인가요?",
Default: "Gopher",
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("프롬프트 실패 %v\n", err)
return
}
fmt.Printf("반가워요, %s님!\n", result)
}
이 코드는 사용자에게 "당신의 이름은 무엇인가요?"라고 묻고 입력을 기다려. 기본값도 설정할 수 있지.
2. Select: 목록에서 하나 선택하기
// ...
func main() {
prompt := promptui.Select{
Label: "가장 좋아하는 프로그래밍 언어는?",
Items: []string{"Go", "Python", "JavaScript", "Rust", "Java"},
}
_, result, err := prompt.Run()
if err != nil {
fmt.Printf("프롬프트 실패 %v\n", err)
return
}
fmt.Printf("당신은 %s를 선택했습니다!\n", result)
}
이걸 실행하면 터미널에 멋진 선택 메뉴가 나타나. 사용자는 화살표 키로 이동하고 엔터 키로 선택할 수 있어. 정말 현대적인 CLI 느낌이 나지? 🤩
3. Confirm: 예/아니오 질문하기
// ...
func main() {
prompt := promptui.Prompt{
Label: "정말로 모든 파일을 삭제하시겠습니까?",
IsConfirm: true,
}
_, err := prompt.Run()
if err != nil {
// 사용자가 'n' 또는 'N'을 입력하면 에러가 발생합니다.
// promptui.ErrAbort가 바로 그 에러.
fmt.Println("작업이 취소되었습니다.")
return
}
fmt.Println("모든 파일이 삭제되었습니다. (실제로는 아니지만요!)")
}
`IsConfirm: true` 옵션 하나만으로 간단한 Y/n 프롬프트를 만들 수 있어. 위험한 작업을 수행하기 전에 사용자에게 한 번 더 확인받는 건 좋은 습관이야.
📊 4. 데이터를 깔끔하게 보여주자: 테이블 출력 `olekukonko/tablewriter`
여러 개의 데이터를 리스트 형태로 출력할 때, 그냥 `fmt.Println`으로 줄줄이 찍어내면 가독성이 떨어져. 이럴 땐 표(Table) 형태로 정리해서 보여주는 게 훨씬 프로페셔널해 보여. `olekukonko/tablewriter`는 이 작업을 아주 쉽게 만들어줘.
$ go get github.com/olekukonko/tablewriter
package main
import (
"os"
"github.com/olekukonko/tablewriter"
)
func main() {
data := [][]string{
{"1", "Gopher", "Go Developer", "1001"},
{"2", "Ferris", "Rust Developer", "2002"},
{"3", "Duke", "Java Developer", "3003"},
{"4", "Moby", "Docker Specialist", "4004"},
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Name", "Role", "Employee ID"})
for _, v := range data {
table.Append(v)
}
table.Render() // 이 함수가 호출되면 테이블이 그려집니다!
}
실행 결과는 다음과 같이 깔끔한 표로 출력될 거야.
+----+--------+---------------------+-------------+
| ID | NAME | ROLE | EMPLOYEE ID |
+----+--------+---------------------+-------------+
| 1 | Gopher | Go Developer | 1001 |
| 2 | Ferris | Rust Developer | 2002 |
| 3 | Duke | Java Developer | 3003 |
| 4 | Moby | Docker Specialist | 4004 |
+----+--------+---------------------+-------------+
정렬, 셀 병합, 색상 지정 등 다양한 옵션도 제공하니, 복잡한 데이터를 시각화해야 할 때 꼭 사용해봐.
재능 공유 플랫폼을 활용해보세요! 오늘 배운 내용들을 활용해서 멋진 CLI 툴을 만들었다면, 그걸로 끝내지 마세요. 여러분의 지식과 경험은 그 자체로 훌륭한 자산이 될 수 있습니다. 예를 들어, 재능넷 같은 플랫폼에 'Go 언어 CLI 개발 멘토링'이나 '맞춤형 자동화 스크립트 제작' 같은 서비스를 등록해서 부가적인 수익을 창출하거나 다른 사람들에게 도움을 줄 수도 있답니다. 배움은 나누면 배가 되니까요!
지금까지 우리는 CLI의 겉모습을 화려하게 꾸미고 사용자와 상호작용하는 법을 배웠어. 하지만 진정한 고수는 겉모습뿐만 아니라 내실도 튼튼하게 다지는 법이지. 다음 챕터에서는 설정 관리, 테스트, 배포 등 우리 CLI를 한 단계 더 성숙시키는 '고급 기술'들을 다뤄볼 거야. 거의 다 왔어, 조금만 더 힘내자! 💪
4단계: 프로의 품격 - 설정, 테스트, 그리고 배포 자동화
이제 우리 CLI는 꽤 그럴듯한 모습을 갖췄어. 멋진 UI와 인터랙션까지 겸비했지. 하지만 아직 '프로'라고 불리기엔 2% 부족해. 이번 챕터에서는 그 2%를 채워줄 고급 주제들을 다룰 거야. 사용자가 설정을 쉽게 관리하게 만들고, 우리 코드가 버그 없이 튼튼하게 작동하는지 검증하고, 마지막으로 세상 사람들이 우리 툴을 쉽게 설치하고 사용할 수 있도록 배포하는 과정까지. 이 단계까지 마스터하면, 당신은 더 이상 주니어가 아닐 거야. 😉
⚙️ 1. 설정 관리의 달인 되기: Viper
지금까지 우리는 플래그를 통해 모든 설정을 커맨드 라인에서 직접 받았어. 하지만 프로그램이 복잡해지면 설정해야 할 값들이 수십 개가 될 수도 있어. 매번 `--db-host=... --db-port=... --api-key=...` 이렇게 긴 명령어를 치는 건 너무 고통스럽잖아. 이럴 때 필요한 게 바로 '설정 파일'이야.
Viper는 Go 애플리케이션을 위한 완벽한 설정 관리 솔루션이야. Cobra를 만든 동일한 개발자(Steve Francia)가 만들어서 둘의 궁합은 환상적이지. Viper의 특징은 정말 놀라워.
- 다양한 설정 파일 포맷 지원 (JSON, TOML, YAML, HCL, INI 등)
- 환경 변수에서 설정값 읽어오기
- 커맨드 라인 플래그에서 설정값 읽어오기
- 설정 파일 실시간 감시 및 자동 리로드 (hot-reloading)
- 기본값 설정 기능
이 모든 것들이 서로 유기적으로 연결돼. Viper는 (1)플래그 → (2)환경 변수 → (3)설정 파일 → (4)기본값 순서로 우선순위를 두고 설정값을 찾아. 즉, 사용자가 플래그로 값을 주면 그게 최우선이고, 없으면 환경 변수를, 그것도 없으면 설정 파일을 찾아보는 식이야. 정말 유연하고 강력하지?
Viper를 우리 프로젝트에 통합해 보자.
$ go get github.com/spf13/viper
먼저, `config.yaml` 이라는 설정 파일을 프로젝트 루트에 만들어 보자.
author: "Jaenung Kim"
license: "MIT"
greeting:
defaultName: "YAML Config"
shout: true
이제 `root.go`를 수정해서 Viper를 초기화하고, `hello.go`에서 Viper를 통해 설정값을 읽어오도록 만들어 볼 거야.
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper" // Viper 임포트
)
var cfgFile string // 설정 파일 경로를 담을 변수
var rootCmd = &cobra.Command{
Use: "my-cli",
Short: "A brief description of your application",
// ...
}
func Execute() {
// ...
}
func init() {
// cobra.OnInitialize는 모든 커맨드의 init() 함수가 실행된 후,
// Execute()가 호출되기 전에 실행되는 함수를 등록합니다.
// Viper 초기화를 하기에 가장 좋은 위치입니다.
cobra.OnInitialize(initConfig)
// --config 플래그를 전역 플래그로 추가합니다.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.my-cli.yaml)")
}
// initConfig는 설정 파일을 읽고 Viper를 초기화하는 함수입니다.
func initConfig() {
if cfgFile != "" {
// --config 플래그로 지정된 설정 파일을 사용합니다.
viper.SetConfigFile(cfgFile)
} else {
// 사용자의 홈 디렉토리를 찾습니다.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// 홈 디렉토리에서 ".my-cli.yaml" 파일을 찾습니다.
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".my-cli")
}
// 환경 변수도 자동으로 읽어오도록 설정합니다.
// MYCLI_AUTHOR=... 와 같은 형식으로 설정할 수 있습니다.
viper.SetEnvPrefix("MYCLI") // 접두사 설정
viper.AutomaticEnv()
// 설정 파일을 읽습니다.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
코드가 조금 길어졌지만, 핵심은 `initConfig` 함수야. 이 함수는 `--config` 플래그가 있으면 그 파일을, 없으면 홈 디렉토리에서 `.my-cli.yaml` 파일을 찾아서 읽어 들여. `viper.AutomaticEnv()` 덕분에 환경 변수도 자동으로 인식하게 됐어.
이제 `hello.go`에서 이 설정값을 사용해 보자.
package cmd
import (
// ...
"github.com/spf13/viper" // Viper 임포트
)
// ... (var shout bool 삭제)
var helloCmd = &cobra.Command{
Use: "hello [name]",
// ...
Run: func(cmd *cobra.Command, args []string) {
var name string
if len(args) > 0 {
name = args[0]
} else {
// 설정 파일에서 기본 이름을 가져옵니다.
name = viper.GetString("greeting.defaultName")
}
// 플래그에서 shout 값을 가져옵니다.
shout, _ := cmd.Flags().GetBool("shout")
greeting := fmt.Sprintf("Hello, %s!", name)
// 플래그가 명시적으로 주어지지 않았다면, 설정 파일의 값을 확인합니다.
if !cmd.Flags().Changed("shout") && viper.GetBool("greeting.shout") {
shout = true
}
if shout {
greeting = strings.ToUpper(greeting)
}
color.Green(greeting)
// 설정 파일에서 author 정보도 가져와서 출력해볼까요?
author := viper.GetString("author")
if author != "" {
fmt.Printf("Brought to you by %s\n", author)
}
},
}
func init() {
rootCmd.AddCommand(helloCmd)
// 기본값을 false로 설정합니다.
helloCmd.Flags().BoolP("shout", "s", false, "Shout the greeting")
// Cobra 플래그와 Viper 설정을 바인딩할 수도 있습니다.
// 이렇게 하면 플래그 값을 가져오는 로직이 더 간단해집니다.
// viper.BindPFlag("greeting.shout", helloCmd.Flags().Lookup("shout"))
}
이제 우리 `hello` 커맨드는 훨씬 똑똑해졌어. 인자 없이 실행하면 `config.yaml`에 정의된 `greeting.defaultName`을 사용하고, `--shout` 플래그가 없어도 `config.yaml`의 `greeting.shout`가 `true`라면 대문자로 외쳐줄 거야. 사용자는 이제 자신의 취향에 맞게 설정 파일을 만들어두고 편하게 CLI를 사용할 수 있게 된 거지. 이게 바로 프로페셔널한 CLI의 기본 소양이야.
🧪 2. 코드에 대한 자신감, 테스트 작성하기
기능이 복잡해질수록 '이거 수정했는데 다른 데서 문제 생기는 거 아니야?' 하는 불안감이 커지기 마련이야. 이 불안감을 해소해 줄 유일한 방법은 바로 '자동화된 테스트'야. Go는 언어 차원에서 테스팅을 매우 강력하게 지원해. `go test` 명령어 하나면 모든 게 해결되지.
CLI 애플리케이션을 테스트하는 건 조금 까다로울 수 있어. 왜냐하면 표준 출력(stdout)으로 결과를 내보내고, 표준 입력(stdin)으로 입력을 받으며, 커맨드 라인 인자에 의존하기 때문이지. 하지만 몇 가지 트릭을 사용하면 이것도 충분히 테스트할 수 있어.
핵심 아이디어는 실제 출력 스트림을 가로채서 버퍼에 저장한 뒤, 그 버퍼의 내용이 우리가 기대하는 결과와 일치하는지 확인하는 거야. Cobra 커맨드의 `SetOut`, `SetErr`, `SetArgs` 메소드가 바로 이럴 때 사용하라고 있는 것들이지.
`cmd` 디렉토리 안에 `hello_test.go` 파일을 만들어 보자. (Go에서 테스트 파일은 `_test.go`로 끝나야 해)
package cmd
import (
"bytes"
"strings"
"testing"
)
func TestHelloCommand(t *testing.T) {
// 테스트 케이스들을 테이블 형태로 정의합니다.
testCases := []struct {
name string // 테스트 이름
args []string // 커맨드에 전달할 인자
expected string // 기대하는 출력 결과
}{
{
name: "no arguments",
args: []string{},
expected: "Hello, World!",
},
{
name: "with one argument",
args: []string{"Gopher"},
expected: "Hello, Gopher!",
},
{
name: "with shout flag",
args: []string{"Gopher", "--shout"},
expected: "HELLO, GOPHER!",
},
{
name: "with shorthand shout flag",
args: []string{"Gopher", "-s"},
expected: "HELLO, GOPHER!",
},
}
// 정의된 테스트 케이스들을 순회하며 실행합니다.
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 1. 출력을 가로챌 버퍼를 생성합니다.
var buf bytes.Buffer
rootCmd.SetOut(&buf) // rootCmd의 출력 대상을 우리 버퍼로 변경
// 2. 테스트할 인자를 설정합니다.
rootCmd.SetArgs(append([]string{"hello"}, tc.args...))
// 3. 커맨드를 실행합니다.
// 실제 main.go의 Execute()와 유사하지만, 에러 발생 시 os.Exit(1)을 하지 않도록
// rootCmd.Execute()를 직접 호출합니다.
err := rootCmd.Execute()
if err != nil {
t.Fatalf("command execution failed: %v", err)
}
// 4. 버퍼에 저장된 실제 출력 결과를 가져옵니다.
// TrimSpace를 사용해 앞뒤 공백을 제거해줍니다.
actual := strings.TrimSpace(buf.String())
// 5. 실제 결과와 기대 결과를 비교합니다.
if actual != tc.expected {
t.Errorf("expected output: %q, but got: %q", tc.expected, actual)
}
})
}
}
이 코드는 '테이블 기반 테스트(Table-Driven Test)'라는 Go에서 널리 쓰이는 패턴을 사용했어. 여러 테스트 케이스를 `struct` 슬라이스로 정의해두고, 루프를 돌면서 하나씩 테스트하는 방식이야. 코드가 깔끔하고 테스트 케이스를 추가하기도 쉬워.
이제 프로젝트 루트에서 `go test` 명령어를 실행해 보자.
$ go test ./...
? my-cli [no test files]
ok my-cli/cmd 0.010s
`ok`가 보인다면 테스트가 성공적으로 통과한 거야! 이제 `hello` 커맨드의 로직을 마음껏 리팩토링해도 돼. 테스트가 우리의 코드를 든든하게 지켜줄 테니까. 이런 테스트 코드를 꾸준히 작성하는 습관은 좋은 개발자의 필수 덕목이야. ✨
🚀 3. 세상에 너를 알려라! 배포 자동화와 GoReleaser
자, 이제 우리의 CLI는 기능도 완벽하고 테스트까지 통과했어. 이제 친구나 동료에게 "이거 한번 써봐!" 하고 자랑하고 싶지 않아? 어떻게 전달해 줄 거야? 소스 코드를 통째로 보내서 `go build` 하라고 할 건가? 그건 너무 불친절하잖아.
Go의 가장 큰 장점 중 하나는 '크로스 컴파일'이 엄청나게 쉽다는 거야. 내 맥북에서 윈도우용, 리눅스용 실행 파일을 간단하게 만들 수 있어. 예를 들어, 윈도우 64비트용 실행 파일을 만들고 싶다면?
$ GOOS=windows GOARCH=amd64 go build -o my-cli.exe
리눅스 ARM 64비트용(라즈베리파이 같은) 실행 파일을 만들고 싶다면?
$ GOOS=linux GOARCH=arm64 go build -o my-cli
이걸 이용해서 각 운영체제(OS)와 아키텍처(Arch)에 맞는 바이너리를 모두 만들고, 압축해서, GitHub 릴리즈 같은 곳에 올려주면 사용자들이 편하게 다운로드해서 쓸 수 있겠지. 하지만 이 모든 과정을 수동으로 하는 건 너무 번거로워. 이럴 때 구세주처럼 등장하는 게 바로 GoReleaser야.
GoReleaser는 Go 프로젝트의 릴리즈 프로세스를 완전히 자동화해주는 미친 도구야. 설정 파일 하나만 만들어두면 다음과 같은 작업들을 명령어 한 방에 처리해줘.
- 다양한 OS/Arch 조합으로 크로스 컴파일
- 컴파일된 바이너리를 OS별로 압축 (`.zip`, `.tar.gz`)
- 체크섬(checksum) 파일 생성 (파일 무결성 검증용)
- GitHub/GitLab 릴리즈 생성 및 파일 업로드
- Homebrew, Scoop, Snapcraft 같은 패키지 매니저용 설치 스크립트 생성
- 도커 이미지 빌드 및 푸시
- 릴리즈 노트 자동 생성
정말 엄청나지? GoReleaser를 설치하고 우리 프로젝트에 적용해 보자.
# macOS (Homebrew)
$ brew install goreleaser
# 다른 OS는 공식 문서를 참고하세요.
설치가 끝났다면, 프로젝트 루트에서 `goreleaser init` 명령어로 기본 설정 파일을 생성하자.
$ goreleaser init
그러면 `.goreleaser.yml` 파일이 생성될 거야. 이 파일을 우리 프로젝트에 맞게 조금 수정해 보자.
# .goreleaser.yml
# 이 파일은 GoReleaser의 작동 방식을 정의합니다.
# 더 많은 옵션은 https://goreleaser.com/customization/ 에서 확인하세요.
before:
hooks:
# go mod tidy를 실행하여 의존성을 정리합니다.
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin # macOS
goarch:
- amd64
- arm64
# main 패키지의 위치를 지정합니다.
main: .
# 생성될 바이너리의 이름을 지정합니다.
binary: my-cli
archives:
- format: tar.gz
# 윈도우용은 zip으로 압축하도록 오버라이드합니다.
format_overrides:
- goos: windows
format: zip
# 압축 파일에 포함될 파일들을 지정합니다.
files:
- LICENSE
- README.md
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
이제 모든 준비는 끝났어. 코드를 Git으로 커밋하고, 태그를 하나 달아보자. GoReleaser는 Git 태그를 버전으로 인식하거든.
$ git add .
$ git commit -m "feat: Initial commit with GoReleaser setup"
$ git tag -a v0.1.0 -m "First release"
$ git push origin main --tags
마지막으로, GitHub에 리포지토리를 만들고, Personal Access Token을 발급받아서 환경 변수로 설정해야 해. (GoReleaser가 GitHub API를 통해 릴리즈를 생성해야 하니까)
$ export GITHUB_TOKEN="your_github_token_here"
그리고 대망의 릴리즈 명령어를 실행하는 거야!
$ goreleaser release --clean
이 명령을 실행하면, GoReleaser가 마법을 부리기 시작할 거야. 컴파일, 압축, 체크섬 생성, GitHub 릴리즈 생성 및 업로드까지... 모든 과정이 자동으로 진행돼. 잠시 후 당신의 GitHub 리포지토리 'Releases' 탭에 가보면, v0.1.0 릴리즈와 함께 각 OS/Arch 별로 다운로드할 수 있는 파일들이 예쁘게 올라가 있는 걸 확인할 수 있을 거야. 벅차오르지 않아? 🥹
이로써 우리는 Go로 사용자 친화적인 CLI를 만드는 여정의 거의 모든 단계를 마쳤어. 기본기부터 시작해서, 강력한 프레임워크로 뼈대를 세우고, 화려한 UI/UX로 살을 붙이고, 마지막으로 프로페셔널한 관리/테스트/배포 시스템으로 영혼까지 불어넣었지. 이제 당신의 아이디어만 있다면, 세상에 없는 멋진 개발자 도구를 만들어낼 수 있는 능력을 갖추게 된 거야.
마치며: 이제 당신의 터미널은 캔버스다
와, 정말 긴 여정이었어! 여기까지 함께 달려온 당신에게 진심으로 박수를 보냅니다. 👏
우리는 Go의 표준 `flag` 패키지라는 작은 씨앗에서 시작해서, Cobra라는 튼튼한 나무로 키워냈어. 그리고 `fatih/color`, `promptui`, `progressbar` 같은 라이브러리들로 화려한 꽃과 잎을 피웠지. 마지막으로 Viper, Go의 테스팅 도구, GoReleaser라는 비옥한 토양과 햇빛으로 이 나무가 지속 가능하게 성장할 수 있는 환경까지 만들어줬어.
이제 당신에게 터미널은 더 이상 명령어를 입력하는 딱딱한 공간이 아닐 거야. 당신의 아이디어를 표현하고, 동료 개발자들의 작업을 편리하게 만들어주며, 복잡한 시스템을 자동화하는 '캔버스'가 되었을 거야. 당신이 만든 CLI 툴이 누군가의 반복적인 업무를 줄여주고, 누군가에게는 개발의 즐거움을 선사할 수도 있어.
기억해. 가장 좋은 도구는 사용자를 생각하는 마음에서 탄생한다는 것을. 도움말은 친절하게, 에러 메시지는 명확하게, 진행 상황은 시각적으로, 설정은 유연하게. 오늘 배운 모든 기술들은 결국 '사용자'를 위한 것이었어.
여기서 멈추지 마. 오늘 배운 것들을 조합해서 당신만의 프로젝트를 시작해봐.
예를 들면 이런 것들?
- 특정 디렉토리의 중복 파일을 찾아 정리해주는 툴
- 여러 클라우드 서비스의 비용을 통합해서 보여주는 대시보드 CLI
- 나만의 블로그 글을 템플릿으로 쉽게 생성하고 배포하는 툴
- 자주 사용하는 Git 명령어들을 조합해서 더 편리하게 만들어주는 래퍼(wrapper) 툴
가능성은 무한해. 그리고 그 가능성을 현실로 만들 수 있는 힘이 이제 당신 손에 있어. Go 언어와 오늘 배운 강력한 생태계 도구들이 당신의 든든한 지원군이 되어줄 거야.
더 깊이 탐험하고 싶다면?
오늘 다룬 내용 외에도 Go CLI 개발의 세계는 넓고 깊어. 더 나아가고 싶다면 아래 키워드들을 탐구해보는 걸 추천해.
- Charm: Bubble Tea, Lip Gloss, Glow 등 터미널을 위한 완벽한 UI 프레임워크. TUI(Text-based User Interface) 애플리케이션을 만들고 싶다면 최종 보스 격이야.
- gRPC / Protobuf: 다른 서비스와 통신하는 CLI를 만들 때, 효율적이고 강력한 RPC 프레임워크.
- Go Plugin System: 사용자가 직접 기능을 확장할 수 있는 플러그인 아키텍처를 CLI에 도입하는 방법.
- Cobra Generator 커스터마이징: `cobra-cli`가 생성하는 코드 템플릿을 직접 수정해서 우리 팀만의 표준 CLI 구조를 만드는 방법.
이제 당신의 차례야. 터미널이라는 캔버스 위에 당신의 상상력을 마음껏 펼쳐봐.
Happy Hacking! 🚀
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.