V vs Go: Hello World부터 빌드까지

V vs Go: Hello World부터 빌드까지

V 언어를 배우다 보면 "어, 이거 Go랑 비슷한데?" 라는 느낌을 자주 받는다. 실제로 V는 Go의 영향을 많이 받았다. V 공식 사이트에서도 "Go를 안다면 V 문법의 약 80%를 이미 아는 것"이라고 말한다. 그렇다면 두 언어는 정확히 어디가 같고 어디가 다를까? 같은 코드를 나란히 놓고 비교해보자.

이 글에서 비교하는 것

이 글은 V와 Go를 객관적으로 비교한다. "어느 언어가 더 좋다"를 말하려는 게 아니라, 각자의 특징과 차이를 코드로 보여주는 것이 목적이다. 두 언어 중 하나만 알아도, 다른 언어가 어떤 접근을 취하는지 이해할 수 있다.

비교할 항목은 다음과 같다.

항목 핵심 질문
설치 얼마나 쉽게 시작할 수 있나?
Hello World 최소한의 코드로 뭘 할 수 있나?
변수와 상수 값을 어떻게 저장하나?
함수 로직을 어떻게 묶나?
에러 처리 실패를 어떻게 다루나?
컬렉션 배열과 맵을 어떻게 쓰나?
동시성 여러 작업을 어떻게 동시에 하나?
컴파일과 바이너리 결과물은 어떻게 다르나?

1. 설치

V 설치

# Linux/macOS — Git에서 소스 다운로드 후 빌드
git clone https://github.com/vlang/v
cd v
make

# 또는 패키지 매니저
# macOS
brew install vlang

# Windows — zip 다운로드 후 압축 해제

Go 설치

# Linux
wget https://go.dev/dl/go1.22.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.linux-amd64.tar.gz

# macOS
brew install go

# Windows — 공식 사이트에서 MSI 인스톨러 다운로드

비교

항목 V Go
설치 방식 소스 빌드 or 바이너리 공식 인스톨러 or 바이너리
설치 용량 ~80MB ~500MB
의존성 C 컴파일러(gcc/clang) 필요 없음 (독립 실행)
안정성 활발한 개발 중 (0.x 버전) 매우 안정 (1.22+)

V는 용량이 작지만 C 컴파일러가 별도로 필요하고, Go는 용량이 크지만 설치가 간단하고 독립적이다. Go는 10년 이상 성숙한 언어이므로 설치 경험이 매우 매끄럽다.


2. Hello World

프로그래밍 언어의 첫인상 — Hello World를 비교해보자.

V

fn main() {
    println('Hello, World!')
}

Go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

차이 분석

두 코드를 나란히 놓으면 흥미로운 차이가 보인다.

요소 V Go
패키지 선언 없음 (자동) package main 필수
import println은 내장, import 불필요 fmt 패키지 import 필요
함수 선언 fn func
출력 함수 println() fmt.Println()
문자열 따옴표 작은따옴표 '...' 큰따옴표 "..."
세미콜론 없음 없음 (같음)

V 쪽이 더 간결하다. println이 내장 함수라서 import가 필요 없고, package 선언도 없다. Go는 "명시적인 것이 좋다"는 철학으로 package mainimport "fmt"를 항상 적는다.

💡 어느 쪽이 더 나은가? 정답은 없다. V의 접근은 "보일러플레이트를 줄이자"이고, Go의 접근은 "모든 것을 명확하게 표현하자"이다. 프로젝트가 커지면 Go의 명시성이 장점이 되기도 하고, 반대로 간단한 스크립트에서는 V의 간결함이 장점이 된다.

실행 방법 비교

# V — 컴파일 + 실행을 한 번에
v run hello.v

# Go — 마찬가지로 한 번에
go run hello.go

이 부분은 거의 동일하다. 두 언어 모두 run 명령 하나로 즉시 실행할 수 있다.


3. 변수와 상수

데이터를 저장하는 방법을 비교해보자.

V

fn main() {
    // 불변 변수 (기본)
    name := '홍길동'
    age := 25

    // 가변 변수 — mut 필요
    mut count := 0
    count = 1
    count += 1

    // 상수
    // const로 선언 (함수 밖에서)
    println('${name}, ${age}세, count=${count}')
}

const max_size = 100

Go

package main

import "fmt"

func main() {
    // 짧은 선언 — 기본이 가변
    name := "홍길동"
    age := 25

    count := 0
    count = 1
    count += 1

    // 상수
    const maxSize = 100

    fmt.Printf("%s, %d세, count=%d\n", name, age, count)
}

차이 분석

요소 V Go
선언 문법 := := (같음)
기본 가변성 불변 (mut 붙여야 가변) 가변 (기본이 가변)
변수 재할당 mut 필수 그냥 =
상수 const (함수 밖) const (함수 안팎 모두 가능)
문자열 보간 '${name}' (내장) fmt.Sprintf("%s", name)
사용하지 않는 변수 경고 (빌드 계속) 컴파일 에러

가장 큰 차이는 기본 가변성이다. V는 "변경하고 싶으면 mut을 명시해라"라는 안전 우선 접근이고, Go는 ":=로 선언하면 당연히 바꿀 수 있다"는 편의 우선 접근이다.

문자열 보간도 큰 차이다. V는 '${name}'으로 깔끔하게 쓰는 반면, Go는 fmt.Sprintf("%s %d", name, age)처럼 포맷 문자열을 써야 한다. Go를 쓰다 V를 쓰면 이 부분이 특히 편하게 느껴진다.


4. 함수

로직을 묶는 핵심 단위인 함수를 비교해보자.

V

// 기본 함수
fn add(a int, b int) int {
    return a + b
}

// 다중 반환
fn divide(a f64, b f64) (f64, bool) {
    if b == 0 {
        return 0.0, false
    }
    return a / b, true
}

fn main() {
    sum := add(3, 5)
    println(sum)   // 8

    result, ok := divide(10.0, 3.0)
    if ok {
        println(result)   // 3.333...
    }
}

Go

package main

import "fmt"

// 기본 함수
func add(a int, b int) int {
    return a + b
}

// 다중 반환
func divide(a float64, b float64) (float64, bool) {
    if b == 0 {
        return 0.0, false
    }
    return a / b, true
}

func main() {
    sum := add(3, 5)
    fmt.Println(sum)   // 8

    result, ok := divide(10.0, 3.0)
    if ok {
        fmt.Println(result)   // 3.333...
    }
}

차이 분석

거의 같다! 두 언어의 함수 문법은 정말 유사하다.

요소 V Go
키워드 fn func
타입 위치 매개변수 뒤: a int 같음: a int
다중 반환 (f64, bool) (float64, bool)
타입 이름 f64, int, bool float64, int, bool
이름 없는 반환 미지원 (result float64, err error) 가능

함수 정의와 다중 반환 문법이 거의 동일하다. 차이는 키워드(fn vs func)와 타입 이름(f64 vs float64) 정도다. Go를 아는 사람이라면 V의 함수를 보는 순간 "아, 이거 바로 읽히는데?"라고 느낄 것이다.


5. 에러 처리 — 가장 큰 차이

여기서 두 언어의 철학적 차이가 가장 크게 드러난다.

V

import os

fn read_config(path string) !string {
    // !로 에러 전파 — 한 줄이면 끝
    content := os.read_file(path)!
    return content
}

fn main() {
    // 방법 1: or 블록
    content := read_config('config.txt') or {
        println('에러: ${err}')
        return
    }
    println(content)

    // 방법 2: 기본값 제공
    data := read_config('data.txt') or { '기본값' }
    println(data)
}

Go

package main

import (
    "fmt"
    "os"
)

func readConfig(path string) (string, error) {
    content, err := os.ReadFile(path)
    if err != nil {
        return "", err
    }
    return string(content), nil
}

func main() {
    content, err := readConfig("config.txt")
    if err != nil {
        fmt.Println("에러:", err)
        return
    }
    fmt.Println(content)
}

차이 분석

이것이 V와 Go의 가장 큰 차이점이다. 같은 동작을 하는 코드를 비교해보자.

에러 전파 코드:

V:    content := os.read_file(path)!       ← 1줄
Go:   content, err := os.ReadFile(path)    ← 3줄
      if err != nil {
          return "", err
      }
요소 V Go
에러 표현 !T (Result 타입) (T, error) (다중 반환)
에러 전파 ! 한 글자 if err != nil { return err }
에러 처리 or {} 블록 if err != nil {}
기본값 제공 or { '기본값' } 직접 분기 코드 작성
null 존재 없음 nil 존재
에러 무시 방지 컴파일러가 강제 무시 가능 (_)

Go에서는 모든 에러를 직접 검사해야 한다. if err != nil 패턴이 Go 코드의 상당 부분을 차지한다. Go 개발자 사이에서는 "전체 코드의 30%가 에러 처리"라는 우스갯소리가 있을 정도다.

V는 이 문제를 언어 차원에서 해결했다. ! 한 글자로 에러를 전파하고, or {} 블록으로 처리한다. 문법 시리즈 8편에서 자세히 배운 부분이다.

💡 공평하게 말하면: Go의 if err != nil 패턴이 반복적이긴 하지만, 에러 처리 지점이 코드에 명확하게 드러난다는 장점도 있다. V의 !는 편리하지만, 에러 전파 지점을 놓칠 수 있다는 주장도 있다. 결국 "명시성 vs 간결성"의 트레이드오프다.


6. 컬렉션 — 배열과 맵

배열

// V
fn main() {
    // 배열 선언
    nums := [1, 2, 3, 4, 5]

    // for...in 순회
    for i, n in nums {
        println('${i}: ${n}')
    }

    // 내장 메서드
    doubled := nums.map(it * 2)          // [2, 4, 6, 8, 10]
    evens := nums.filter(it % 2 == 0)    // [2, 4]
    println(doubled)
    println(evens)
}
// Go
package main

import "fmt"

func main() {
    // 슬라이스 선언
    nums := []int{1, 2, 3, 4, 5}

    // range 순회
    for i, n := range nums {
        fmt.Printf("%d: %d\n", i, n)
    }

    // map/filter — 내장 없음, 직접 구현
    doubled := make([]int, len(nums))
    for i, n := range nums {
        doubled[i] = n * 2
    }

    var evens []int
    for _, n := range nums {
        if n % 2 == 0 {
            evens = append(evens, n)
        }
    }
    fmt.Println(doubled)
    fmt.Println(evens)
}

차이 분석

이 비교에서 코드 길이 차이가 극적으로 드러난다.

요소 V Go
배열 선언 [1, 2, 3] []int{1, 2, 3} (타입 명시)
순회 for i, n in arr for i, n := range arr
map arr.map(it * 2) — 1줄 직접 for문 — 3~4줄
filter arr.filter(it > 3) — 1줄 직접 for문 — 4~5줄
sort arr.sorted() sort.Ints(arr) (별도 패키지)
it 키워드 있음 (현재 요소) 없음

V의 배열은 map, filter, sort, any, all 등의 메서드를 내장하고 있다. Go는 이런 함수형 메서드가 없어서 for 루프를 직접 작성해야 한다. (Go 1.21부터 slices 패키지에 일부 추가되었지만, V만큼 간결하지는 않다.)

V의 it 키워드도 독특하다. map(it * 2)에서 it은 "현재 요소"를 뜻한다. Go에서는 이런 단축 문법이 없다.

// V
fn main() {
    scores := {
        '국어': 90
        '영어': 85
        '수학': 95
    }

    println(scores['국어'])   // 90
}
// Go
package main

import "fmt"

func main() {
    scores := map[string]int{
        "국어": 90,
        "영어": 85,
        "수학": 95,
    }

    fmt.Println(scores["국어"])   // 90
}

맵은 큰 차이가 없지만, V가 타입 추론으로 좀 더 간결하다.


7. 동시성

두 언어 모두 동시성 프로그래밍을 핵심 기능으로 지원한다.

V

import time

fn worker(name string, ch chan string) {
    time.sleep(1 * time.second)
    ch <- '${name} 완료!'
}

fn main() {
    ch := chan string{cap: 2}

    spawn worker('작업A', ch)
    spawn worker('작업B', ch)

    println(<-ch)
    println(<-ch)
}

Go

package main

import (
    "fmt"
    "time"
)

func worker(name string, ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- name + " 완료!"
}

func main() {
    ch := make(chan string, 2)

    go worker("작업A", ch)
    go worker("작업B", ch)

    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

차이 분석

동시성 문법도 매우 유사하다.

요소 V Go
스레드 실행 spawn fn() go fn()
채널 생성 chan T{cap: N} make(chan T, N)
채널 보내기 ch <- val (같음) ch <- val (같음)
채널 받기 <-ch (같음) <-ch (같음)
반환값 받기 handle.wait() 채널로 전달

채널 문법(ch <-, <-ch)은 완전히 동일하다. V가 Go에서 직접 가져온 문법이기 때문이다. 차이점은 두 가지다.

spawn vs go: V는 spawn을, Go는 go를 쓴다. V의 spawn은 핸들을 반환하므로 handle.wait()로 반환값을 직접 받을 수 있다. Go의 go는 반환값이 없어서 채널로 결과를 보내야 한다.

채널 생성: V는 chan T{cap: N} 구조체 문법을, Go는 make(chan T, N) 내장 함수를 사용한다.


8. 컴파일과 바이너리

컴파일 속도

언어 컴파일 속도 설명
V ~CPU당 120만 줄/초 C 코드 생성 후 C 컴파일러로 최종 빌드
Go 매우 빠름 Go 컴파일러가 직접 기계어 생성

두 언어 모두 컴파일 속도를 핵심 강점으로 내세운다. 두 언어 모두 C++이나 Rust에 비해 압도적으로 빠르다. 실제 개발에서 체감상 큰 차이를 느끼기 어려운 수준이다. 둘 다 "저장 → 빌드 → 실행"이 거의 즉시 이루어진다.

바이너리 크기

이 부분에서 V와 Go의 차이가 극적으로 나타난다.

프로그램 V Go
Hello World ~30KB ~2MB
간단한 웹 서버 ~600KB ~7MB

Go 바이너리가 큰 이유는 런타임을 포함하기 때문이다. Go의 가비지 컬렉터, goroutine 스케줄러, 리플렉션 시스템 등이 바이너리에 같이 들어간다. V는 이런 런타임이 훨씬 가벼워서 바이너리가 작다.

💡 바이너리가 크면 나쁜가? 반드시 그렇지는 않다. 디스크와 메모리가 충분한 서버에서는 7MB나 30KB나 큰 차이가 없다. 하지만 임베디드 시스템, 컨테이너 배포, 서버리스 함수 등 용량이 중요한 환경에서는 V의 작은 바이너리가 큰 장점이 된다.

빌드 명령 비교

# V — 컴파일
v hello.v              # → hello (또는 hello.exe)
v -prod hello.v        # 최적화 빌드

# Go — 컴파일
go build hello.go      # → hello (또는 hello.exe)
go build -ldflags="-s -w" hello.go  # 최적화 빌드 (디버그 정보 제거)

전체 비교 요약

항목 V Go 승자
간결함 매우 간결 명시적 V
생태계 성장 중 매우 풍부 Go
에러 처리 !, or {} (간결) if err != nil (명시적) 취향
배열 메서드 map, filter 내장 for 루프 직접 작성 V
동시성 spawn + 채널 go + 채널 비슷
컴파일 속도 매우 빠름 매우 빠름 비슷
바이너리 크기 매우 작음 큼 (런타임 포함) V
안정성/성숙도 0.x 버전 10년+ 안정 Go
취업 시장 아직 적음 매우 넓음 Go
문서/커뮤니티 성장 중 매우 풍부 Go

어떤 언어를 선택할까?

V를 선택하면 좋은 경우

  • 간결한 코드를 좋아하고, 보일러플레이트를 줄이고 싶다
  • 작은 바이너리가 중요한 환경이다 (임베디드, 컨테이너)
  • Go의 if err != nil 패턴이 불편하다
  • 새로운 언어를 실험하면서 배우는 것을 즐긴다
  • 개인 프로젝트나 CLI 도구를 빠르게 만들고 싶다

Go를 선택하면 좋은 경우

  • 안정성과 성숙도가 중요하다 (프로덕션 서비스)
  • 풍부한 라이브러리 생태계가 필요하다
  • 취업을 목표로 한다 (클라우드, 백엔드 분야에서 수요가 높다)
  • 대규모 팀에서 일관된 코드 스타일이 필요하다
  • Google, AWS, Docker 등 검증된 기업들이 사용하는 언어를 원한다

핵심 정리

V와 Go는 형제 같은 언어다. Go를 알면 V를 빠르게 배울 수 있고, 반대도 마찬가지다. 두 언어는 경쟁 관계라기보다 상호 보완 관계에 가깝다.

  • V는 Go의 장점(빠른 컴파일, 간결한 문법, 동시성)을 가져가면서, 더 간결하고 안전한 방향으로 발전시켰다
  • Go는 10년간의 검증과 거대한 생태계로 안정성과 신뢰를 제공한다

결국 "어떤 언어가 더 좋은가"보다 "이 프로젝트에 어떤 언어가 더 적합한가" 가 올바른 질문이다. 개인 프로젝트에서 V를 써보고, 회사 프로젝트에서 Go를 쓰는 것도 좋은 조합이다.


📝 정리

  • [x] 문법 유사성 — V와 Go는 약 80% 유사, 함수와 동시성 문법은 거의 동일
  • [x] 기본 가변성 — V는 불변 기본(안전), Go는 가변 기본(편의)
  • [x] 에러 처리 — V(!, or {})는 간결, Go(if err != nil)는 명시적
  • [x] 배열 메서드 — V는 map/filter 내장, Go는 직접 for 루프
  • [x] 바이너리 크기 — V가 훨씬 작음 (30KB vs 2MB)
  • [x] 생태계/안정성 — Go가 훨씬 성숙 (10년+ vs 개발 중)
  • [x] 선택 기준 — 간결함·실험 → V, 안정성·취업 → Go

다음 편 예고

실전 3편: vweb으로 REST API 만들기 (상)

비교는 여기까지! 다시 실전으로 돌아가서, V의 내장 웹 프레임워크 vweb을 이용해 REST API를 만든다. 라우팅, GET/POST 처리, JSON 응답 — 2편에서 배운 JSON 처리를 웹 서버에 적용해보자.