V 실전: JSON 처리기 — 설정 파일 관리 도구
1편에서 파일 정리기를 만들면서 확장자 → 카테고리 매핑을 코드 안에 직접 작성했다. 하지만 실전에서는 이런 설정을 외부 파일로 빼는 것이 일반적이다. 설정을 바꿀 때마다 코드를 수정하고 다시 컴파일하는 건 번거로우니까. 이번 편에서는 외부 설정의 표준 언어인 JSON을 V에서 다루는 법을 배우고, 설정 파일 관리 도구를 완성한다.
이번 편에서 만드는 것
설정 파일 관리 도구(Config Manager) — JSON 설정 파일을 읽고, 수정하고, 새로 만드는 CLI 도구다.
# 새 설정 파일 생성
v run config_manager.v init
# 설정 읽기
v run config_manager.v get app_name
# 설정 변경
v run config_manager.v set port 3000
# 전체 설정 보기
v run config_manager.v show
JSON이란?
JSON(JavaScript Object Notation) 은 데이터를 저장하고 전달하는 표준 형식이다. 이름에 "JavaScript"가 들어있지만, 거의 모든 프로그래밍 언어에서 사용된다. 사람도 읽을 수 있고, 컴퓨터도 쉽게 파싱할 수 있어서 데이터 교환의 세계 공용어라고 할 수 있다.
비유하자면 택배 송장이다. 보내는 사람이 한국어를 쓰든 영어를 쓰든, 송장에는 "이름", "주소", "전화번호"라는 공통 형식이 있어서 누구든 읽을 수 있다. JSON도 마찬가지로, V 프로그램이든 Python 서버든 JavaScript 웹앱이든 JSON 형식만 지키면 서로 데이터를 주고받을 수 있다.
{
"app_name": "내 앱",
"port": 8080,
"debug": false,
"authors": ["홍길동", "김영희"]
}
JSON의 기본 규칙을 빠르게 정리하면 이렇다.
| 요소 | JSON 표기 | 의미 |
|---|---|---|
| 객체 | { } | 키-값 쌍의 묶음 (V의 구조체와 비슷) |
| 키 | "app_name" | 항상 큰따옴표로 감싼 문자열 |
| 문자열 값 | "내 앱" | 큰따옴표로 감싼 텍스트 |
| 숫자 값 | 8080 | 따옴표 없이 숫자만 |
| 불리언 값 | true, false | 따옴표 없음 |
| 배열 | ["a", "b"] | 대괄호 안에 값을 나열 |
웹 API의 응답, 설정 파일, 데이터 교환 등 거의 모든 곳에서 쓰인다. V의 json 모듈을 사용하면 이 JSON을 매우 간단하게 처리할 수 있다.
1단계: JSON 기초 — json.decode와 json.encode
JSON → 구조체 (디코딩)
JSON 문자열을 V의 구조체로 변환하는 것을 디코딩(decoding) 이라고 한다. "포장을 뜯어서 내용물을 꺼내는 것"이라고 생각하면 된다.
import json
struct User {
name string
age int
}
fn main() {
// JSON 문자열
data := '{"name": "홍길동", "age": 25}'
// JSON → 구조체로 변환
user := json.decode(User, data) or {
println('JSON 파싱 실패: ${err}')
return
}
println(user.name) // 홍길동
println(user.age) // 25
}
이 코드가 하는 일을 단계별로 풀어보자.
json.decode(User, data)
──── ────
│ └─ JSON 문자열: '{"name": "홍길동", "age": 25}'
└─ 변환할 구조체 타입: User
내부 동작:
JSON의 "name" → User의 name 필드에 "홍길동" 매핑
JSON의 "age" → User의 age 필드에 25 매핑
결과: User{ name: '홍길동', age: 25 }
핵심은 JSON의 키 이름과 구조체의 필드 이름이 같으면 자동으로 매핑된다는 것이다. "name"이라는 JSON 키가 있고 구조체에도 name 필드가 있으니, V가 알아서 연결해준다.
json.decode는 실패할 수 있으므로 !T를 반환한다. (8편에서 배운 Result 타입이다.) 따라서 or {} 블록이 필요하다. 어떤 경우에 실패할까?
- JSON 형식이 잘못되었을 때:
'{name: 홍길동}'(큰따옴표 누락) - JSON에 비어있는 문자열이 올 때:
'' - 타입이 맞지 않을 때:
age에"스물다섯"(문자열)이 오면int로 변환 실패
구조체 → JSON (인코딩)
반대 방향도 가능하다. V 구조체를 JSON 문자열로 변환하는 것을 인코딩(encoding) 이라고 한다. "내용물을 포장하는 것"이다.
import json
struct User {
name string
age int
}
fn main() {
user := User{
name: '김영희'
age: 30
}
// 구조체 → JSON 문자열
output := json.encode(user)
println(output)
// {"name":"김영희","age":30}
}
json.encode(user)
────
└─ User{ name: '김영희', age: 30 }
내부 동작:
User의 name 필드 → JSON의 "name" 키로 변환
User의 age 필드 → JSON의 "age" 키로 변환
결과: '{"name":"김영희","age":30}'
json.encode(구조체) — 구조체를 JSON 문자열로 변환한다. decode와 달리 이 함수는 항상 성공하므로 or {} 블록이 필요 없다. 구조체는 타입이 확정되어 있으니, 변환에 실패할 이유가 없기 때문이다.
💡
decodevsencode기억법: - decode — JSON(외부)에서 구조체(내부)로. "들어오는 데이터 해독" → 실패 가능 (!T) - encode — 구조체(내부)에서 JSON(외부)로. "나가는 데이터 변환" → 항상 성공
예쁘게 출력하기
json.encode()는 한 줄로 압축된 JSON을 출력한다. 사람이 읽기 좋게 들여쓰기(pretty print)를 하려면 json.encode_pretty()를 사용한다.
import json
struct Config {
app_name string
port int
debug bool
}
fn main() {
config := Config{
app_name: '내 앱'
port: 8080
debug: false
}
// 압축 JSON
println(json.encode(config))
// {"app_name":"내 앱","port":8080,"debug":false}
// 예쁜 JSON (들여쓰기 포함)
println(json.encode_pretty(config))
// {
// "app_name": "내 앱",
// "port": 8080,
// "debug": false
// }
}
2단계: 다양한 JSON 구조 다루기
실전에서 JSON은 단순한 키-값만 있지 않다. 배열이 들어있기도 하고, JSON 안에 JSON이 중첩되기도 한다. 각 경우에 V 구조체를 어떻게 설계하면 되는지 알아보자.
배열이 포함된 JSON
JSON에 배열([ ])이 들어있으면, 구조체 필드를 V의 배열 타입으로 선언하면 자동 매핑된다.
import json
struct Project {
name string
version string
tags []string // JSON 배열 → V 배열
}
fn main() {
data := '{
"name": "my-app",
"version": "1.0.0",
"tags": ["web", "api", "v"]
}'
project := json.decode(Project, data) or { return }
println(project.name) // my-app
println(project.tags) // ['web', 'api', 'v']
println(project.tags.len) // 3
// 5편에서 배운 배열 메서드도 당연히 쓸 수 있다
if 'web' in project.tags {
println('웹 프로젝트입니다!')
}
}
JSON 배열(["web", "api", "v"])이 V의 배열([]string)로 자동 변환된다. 변환된 후에는 5편에서 배운 in, filter, map 등 모든 배열 메서드를 그대로 쓸 수 있다.
| JSON 타입 | V 구조체 필드 | 예시 |
|---|---|---|
["a", "b"] (문자열 배열) | []string | tags []string |
[1, 2, 3] (숫자 배열) | []int | scores []int |
[true, false] (불리언 배열) | []bool | flags []bool |
중첩 JSON — 구조체 안의 구조체
JSON 안에 또 다른 JSON 객체가 들어있는 경우가 흔하다. 예를 들어 앱 설정 안에 데이터베이스 설정이 포함된 구조다.
import json
struct Database {
host string
port int
name string
}
struct AppConfig {
app_name string
debug bool
database Database // 중첩 구조체
}
fn main() {
data := '{
"app_name": "my-app",
"debug": true,
"database": {
"host": "localhost",
"port": 5432,
"name": "mydb"
}
}'
config := json.decode(AppConfig, data) or { return }
println(config.app_name) // my-app
println(config.database.host) // localhost
println(config.database.port) // 5432
}
매핑 과정을 시각화하면 이렇다.
JSON: V 구조체:
┌─────────────────────┐ ┌──────────────────────┐
│ "app_name": "my-app" ├──매핑──→ │ app_name: 'my-app' │
│ "debug": true ├──매핑──→ │ debug: true │
│ "database": { │ │ database: Database{ │
│ "host": "localhost"├──매핑──→ │ host: 'localhost' │
│ "port": 5432 ├──매핑──→ │ port: 5432 │
│ "name": "mydb" ├──매핑──→ │ name: 'mydb' │
│ } │ │ } │
└─────────────────────┘ └──────────────────────┘
핵심은 JSON의 중첩 구조를 V의 중첩 구조체로 그대로 반영하면 된다는 것이다. AppConfig 안에 Database 타입의 필드를 넣으면, V가 JSON의 중첩 객체를 자동으로 중첩 구조체에 매핑한다. 접근할 때는 config.database.host처럼 점(.)을 연쇄해서 쓴다.
필드 이름 커스터마이징 — @[json: '...']
현실에서는 JSON의 키 이름과 V 구조체의 필드 이름이 다른 경우가 많다. 대표적인 예가 웹 API 응답이다. JavaScript는 camelCase(statusCode)를 쓰는데, V는 snake_case(status_code)를 쓴다. 이럴 때 @[json] 속성으로 "JSON에서는 이 이름을 써라"고 알려줄 수 있다.
import json
struct ApiResponse {
status_code int @[json: 'statusCode'] // JSON에서는 camelCase
error_msg string @[json: 'errorMessage']
is_success bool @[json: 'isSuccess']
}
fn main() {
data := '{
"statusCode": 200,
"errorMessage": "",
"isSuccess": true
}'
resp := json.decode(ApiResponse, data) or { return }
println(resp.status_code) // 200
println(resp.is_success) // true
}
@[json: 'statusCode']는 "JSON에서 statusCode라는 키를 찾아서 이 필드에 넣어라"라는 뜻이다. V 코드에서는 resp.status_code(snake_case)로 접근하지만, JSON에서는 statusCode(camelCase)를 찾는다.
encode할 때도 마찬가지다. json.encode(resp)를 하면 JSON 출력에 statusCode로 나온다.
💡 이 기능이 필요한 대표적인 상황: - 외부 API의 응답을 파싱할 때 (대부분 camelCase) - JSON 키에 V에서 변수 이름으로 쓸 수 없는 문자가 있을 때 (예:
"first-name") - 기존 JSON 형식을 바꿀 수 없지만, V 코드는 컨벤션에 맞추고 싶을 때
선택적 필드 — 없어도 에러가 나지 않는 필드
JSON에 특정 키가 없을 수 있다면, 해당 필드에 기본값을 지정해두면 된다.
import json
struct Settings {
theme string = 'dark' // JSON에 없으면 'dark' 사용
language string = 'ko' // JSON에 없으면 'ko' 사용
font_size int = 14 // JSON에 없으면 14 사용
}
fn main() {
// theme 키만 있는 불완전한 JSON
data := '{"theme": "light"}'
settings := json.decode(Settings, data) or { return }
println(settings.theme) // light (JSON에서 읽음)
println(settings.language) // ko (기본값)
println(settings.font_size) // 14 (기본값)
}
6편에서 배운 구조체 기본값이 여기서 활용된다. JSON에 빠진 필드는 기본값으로 채워진다.
3단계: JSON + 파일 I/O
지금까지는 JSON 문자열을 코드 안에 직접 적었다. 하지만 실전에서 JSON은 파일에 저장하고 파일에서 읽는 것이 일반적이다. 설정 파일, API 응답 캐시, 사용자 데이터 저장 등 대부분 파일 기반이다.
1편에서 배운 os 모듈의 파일 I/O와 json 모듈을 결합해보자.
JSON 파일 읽기
config.json 파일이 아래와 같다고 가정하자.
{
"app_name": "내 앱",
"port": 8080,
"debug": true
}
이 파일을 V에서 읽어서 구조체로 만드는 코드다.
import os
import json
struct Config {
app_name string
port int
debug bool
}
fn load_config(path string) !Config {
// 1) 파일 전체를 문자열로 읽기
content := os.read_file(path)!
// 2) JSON 문자열 → 구조체
config := json.decode(Config, content)!
return config
}
fn main() {
config := load_config('config.json') or {
println('설정 로드 실패: ${err}')
return
}
println('앱 이름: ${config.app_name}')
println('포트: ${config.port}')
}
load_config 함수를 한 줄씩 풀어보자.
os.read_file(path)! — 파일 전체 내용을 하나의 문자열로 읽는다. 파일이 없거나 읽기 권한이 없으면 에러가 발생하고, !로 즉시 호출한 쪽에 전파된다.
json.decode(Config, content)! — 읽어온 문자열을 Config 구조체로 변환한다. JSON 형식이 잘못되면 에러가 발생하고, 역시 !로 전파된다.
두 줄 모두 !로 에러를 전파하고 있다. 8편에서 배운 에러 전파 체인이 실전에서 얼마나 편한지 느껴지는 부분이다. os.read_file에서 실패하든, json.decode에서 실패하든, 한 줄의 or {} 블록으로 모든 에러를 처리할 수 있다.
JSON 파일 쓰기
import os
import json
struct Config {
app_name string
port int
debug bool
}
fn save_config(path string, config Config) ! {
// 구조체 → 예쁜 JSON 문자열
content := json.encode_pretty(config)
// 문자열을 파일에 쓰기
os.write_file(path, content)!
}
fn main() {
config := Config{
app_name: '내 앱'
port: 8080
debug: true
}
save_config('config.json', config) or {
println('저장 실패: ${err}')
return
}
println('설정 저장 완료!')
}
os.write_file(경로, 내용) — 문자열을 파일에 쓴다. 파일이 없으면 새로 만들고, 있으면 덮어쓴다.
4단계: 완성 — 설정 파일 관리 도구
이제 모든 조각을 합쳐서 진짜 쓸 수 있는 도구를 만들 차례다. 네 가지 명령을 지원하는 설정 관리 도구를 만들자.
| 명령 | 기능 | 예시 |
|---|---|---|
init | 기본 설정 파일 생성 | v run config_manager.v init |
show | 전체 설정 보기 | v run config_manager.v show |
get <키> | 특정 설정값 조회 | v run config_manager.v get port |
set <키> <값> | 특정 설정값 변경 | v run config_manager.v set port 3000 |
전체 코드를 먼저 보여주고, 아래에서 핵심 부분을 하나씩 설명하겠다.
완성 코드
import os
import json
// 설정 구조체
struct AppConfig {
mut:
app_name string = 'my-app'
port int = 8080
debug bool = false
version string = '1.0.0'
language string = 'ko'
}
const config_file = 'app_config.json'
// 설정 파일 로드
fn load_config() !AppConfig {
content := os.read_file(config_file)!
return json.decode(AppConfig, content)
}
// 설정 파일 저장
fn save_config(config AppConfig) ! {
os.write_file(config_file, json.encode_pretty(config))!
}
// init 명령: 기본 설정 파일 생성
fn cmd_init() {
if os.exists(config_file) {
println('⚠️ ${config_file}이 이미 존재합니다.')
println(' 덮어쓰려면 파일을 삭제 후 다시 실행하세요.')
return
}
config := AppConfig{} // 모든 필드 기본값 사용
save_config(config) or {
println('❌ 생성 실패: ${err}')
return
}
println('✅ ${config_file} 생성 완료!')
println('')
println(json.encode_pretty(config))
}
// show 명령: 전체 설정 보기
fn cmd_show() {
config := load_config() or {
println('❌ 설정 파일을 읽을 수 없습니다: ${err}')
println(' "init" 명령으로 먼저 생성하세요.')
return
}
println('📋 현재 설정 (${config_file}):')
println('─'.repeat(40))
println(' app_name : ${config.app_name}')
println(' port : ${config.port}')
println(' debug : ${config.debug}')
println(' version : ${config.version}')
println(' language : ${config.language}')
println('─'.repeat(40))
}
// get 명령: 특정 값 조회
fn cmd_get(key string) {
config := load_config() or {
println('❌ 설정 파일을 읽을 수 없습니다: ${err}')
return
}
value := match key {
'app_name' { config.app_name }
'port' { config.port.str() }
'debug' { config.debug.str() }
'version' { config.version }
'language' { config.language }
else {
println('❌ 알 수 없는 키: "${key}"')
println(' 사용 가능한 키: app_name, port, debug, version, language')
return
}
}
println('${key} = ${value}')
}
// set 명령: 특정 값 변경
fn cmd_set(key string, value string) {
mut config := load_config() or {
println('❌ 설정 파일을 읽을 수 없습니다: ${err}')
return
}
match key {
'app_name' { config.app_name = value }
'port' {
config.port = value.int()
if config.port <= 0 {
println('❌ port는 양수여야 합니다: "${value}"')
return
}
}
'debug' {
config.debug = value == 'true'
}
'version' { config.version = value }
'language' { config.language = value }
else {
println('❌ 알 수 없는 키: "${key}"')
println(' 사용 가능한 키: app_name, port, debug, version, language')
return
}
}
save_config(config) or {
println('❌ 저장 실패: ${err}')
return
}
println('✅ ${key} = ${value} (저장 완료)')
}
// 사용법 출력
fn print_usage() {
println('')
println('📋 설정 파일 관리 도구 v1.0')
println('')
println('사용법:')
println(' config_manager init 새 설정 파일 생성')
println(' config_manager show 전체 설정 보기')
println(' config_manager get <키> 특정 설정값 조회')
println(' config_manager set <키> <값> 특정 설정값 변경')
println('')
println('사용 가능한 키: app_name, port, debug, version, language')
println('')
}
fn main() {
args := os.args
if args.len < 2 {
print_usage()
return
}
command := args[1]
match command {
'init' { cmd_init() }
'show' { cmd_show() }
'get' {
if args.len < 3 {
println('사용법: config_manager get <키>')
return
}
cmd_get(args[2])
}
'set' {
if args.len < 4 {
println('사용법: config_manager set <키> <값>')
return
}
cmd_set(args[2], args[3])
}
else {
println('❌ 알 수 없는 명령: "${command}"')
print_usage()
}
}
}
완성 코드 핵심 부분 설명
코드가 길지만, 구조는 단순하다. 핵심 부분만 짚어보자.
구조체의 mut: 섹션: set 명령이 필드 값을 변경해야 하므로, AppConfig의 필드들을 mut: 섹션에 넣었다. 6편에서 배운 것처럼, mut:이 없으면 필드를 변경할 수 없다.
struct AppConfig {
mut: // ← set 명령에서 변경하기 위해 필요
app_name string = 'my-app'
port int = 8080
// ...
}
cmd_set의 타입 변환: 명령줄에서 받은 값은 항상 문자열이다. port는 정수여야 하고, debug는 불리언이어야 하므로 타입 변환이 필요하다.
'port' {
config.port = value.int() // 문자열 "3000" → 정수 3000
if config.port <= 0 {
println('❌ port는 양수여야 합니다') // 유효성 검사
return
}
}
'debug' {
config.debug = value == 'true' // 문자열 "true" → 불리언 true
}
value.int()는 문자열을 정수로 변환한다. 변환 후에 값이 유효한지(양수인지) 추가로 검사하는 것이 방어적 프로그래밍이다.
main의 명령어 라우팅: match로 첫 번째 인자를 검사해서 적절한 명령 함수를 호출한다.
match command {
'init' { cmd_init() } // 인자 0개 → 그냥 호출
'show' { cmd_show() } // 인자 0개 → 그냥 호출
'get' { cmd_get(args[2]) } // 인자 1개(키) → 키를 넘김
'set' { cmd_set(args[2], args[3]) } // 인자 2개(키, 값)
else { print_usage() } // 모르는 명령 → 도움말
}
실행 예시
# 1. 설정 파일 생성
$ v run config_manager.v init
✅ app_config.json 생성 완료!
{
"app_name": "my-app",
"port": 8080,
"debug": false,
"version": "1.0.0",
"language": "ko"
}
# 2. 전체 설정 보기
$ v run config_manager.v show
📋 현재 설정 (app_config.json):
────────────────────────────────────────
app_name : my-app
port : 8080
debug : false
version : 1.0.0
language : ko
────────────────────────────────────────
# 3. 특정 값 조회
$ v run config_manager.v get port
port = 8080
# 4. 값 변경
$ v run config_manager.v set port 3000
✅ port = 3000 (저장 완료)
$ v run config_manager.v set debug true
✅ debug = true (저장 완료)
set을 실행한 후 app_config.json 파일을 열어보면, 값이 실제로 변경되어 저장되어 있다. 프로그램을 다시 실행해도 변경 사항이 유지된다. 이것이 파일 기반 설정의 핵심 장점이다.
코드 분석: 핵심 패턴 정리
패턴 1: 로드 → 수정 → 저장
설정 파일을 다루는 가장 기본적인 패턴이다.
파일 읽기 (os.read_file)
↓
JSON 파싱 (json.decode)
↓
구조체 필드 변경
↓
JSON 변환 (json.encode_pretty)
↓
파일 쓰기 (os.write_file)
이 패턴은 설정 파일뿐 아니라, 데이터를 파일에 저장하는 모든 프로그램에 적용된다. 게임 세이브 파일, 사용자 프로필, 앱 상태 저장 등.
패턴 2: 명령어 라우팅
match로 인자를 분기하는 패턴은 모든 CLI 도구의 뼈대다.
match command {
'init' { cmd_init() }
'show' { cmd_show() }
'get' { cmd_get(args[2]) }
'set' { cmd_set(args[2], args[3]) }
else { print_usage() }
}
명령어 하나가 함수 하나에 대응한다. 새 명령을 추가할 때 match에 한 줄만 추가하면 된다.
패턴 3: 방어적 인자 검증
'get' {
if args.len < 3 { // 인자 개수 검증
println('사용법: ...')
return
}
cmd_get(args[2])
}
사용자가 인자를 빠뜨릴 수 있으므로, 명령을 실행하기 전에 인자 개수를 확인한다. 이것은 실전 CLI 도구에서 빠뜨리면 안 되는 습관이다.
문법 시리즈 연결
| 활용된 문법 | 배운 편 | 이번 편에서 |
|---|---|---|
| 구조체 정의, 기본값 | 6편 | AppConfig 구조체와 기본값 |
mut 필드 | 6편 | 설정값 변경을 위한 mut: 섹션 |
match 패턴 매칭 | 4편 | 명령어 라우팅과 키 이름 분기 |
!T, or {}, ! 전파 | 8편 | JSON 파싱/파일 I/O 에러 처리 |
os 모듈 | 9편 | read_file, write_file, exists |
os.args | 1편(실전) | 명령줄 인자 파싱 |
🔧 확장 아이디어
1. 1편 파일 정리기에 JSON 설정 적용
1편에서 코드에 하드코딩했던 확장자 → 카테고리 매핑을 JSON 설정 파일로 분리할 수 있다.
{
"categories": {
"Documents": [".pdf", ".txt", ".doc", ".md"],
"Images": [".jpg", ".png", ".gif", ".svg"],
"Music": [".mp3", ".wav", ".flac"]
}
}
사용자가 설정 파일만 편집하면 새로운 확장자를 추가할 수 있다. 코드를 건드리지 않아도 된다.
2. 환경별 설정 관리
config.dev.json, config.prod.json처럼 환경별 설정 파일을 만들고, 인자로 선택하게 할 수 있다.
3. JSON 스키마 검증
설정 파일에 필수 키가 빠져있거나, 잘못된 타입의 값이 들어있을 때 경고를 출력하는 기능을 추가할 수 있다.
📝 정리
이번 글에서 배운 핵심 포인트를 체크리스트로 정리한다.
- [x]
json.decode(타입, 문자열)— JSON → 구조체 변환,!T반환 - [x]
json.encode(구조체)— 구조체 → JSON 변환 (한 줄) - [x]
json.encode_pretty()— 들여쓰기 포함 예쁜 JSON - [x] 배열/중첩 JSON —
[]string필드, 중첩 구조체로 자동 매핑 - [x]
@[json: 'name']— JSON 키 이름과 V 필드 이름이 다를 때 - [x] 선택적 필드 — 구조체 기본값으로 JSON에 없는 키 처리
- [x] 로드→수정→저장 패턴 —
read_file→decode→ 변경 →encode_pretty→write_file - [x] 명령어 라우팅 —
match로 CLI 명령 분기
다음 편 예고
비교 1편: V vs Go — Hello World부터 빌드까지
잠깐 실전을 멈추고, V와 가장 가까운 형제 언어인 Go를 비교해본다. 설치, Hello World, 컴파일 속도, 바이너리 크기, 문법 차이 — 두 언어를 나란히 놓고 객관적으로 살펴본다. Go를 이미 아는 분이라면 V가 어떻게 다른지, Go를 모르는 분이라면 V의 장점이 무엇인지 명확하게 알 수 있을 것이다.