V 언어 문법: 변수, 상수, 기본 타입 완전 정복
1편에서
name := '홍길동'이라는 코드를 보고 ":=이 뭐지?"라고 궁금했다면, 이번 편에서 그 궁금증이 모두 풀린다. V 언어에서 데이터를 담는 "상자"인 변수, 절대 바꿀 수 없는 상수, 그리고 V가 제공하는 다양한 데이터 타입을 하나하나 파헤쳐보자.
변수 선언: := 연산자
프로그래밍에서 변수(variable) 란 값을 담아두는 상자와 같다. 이 상자에 이름표를 붙이고 값을 넣는 행위를 "변수를 선언한다" 고 말한다.
V에서는 := 연산자로 변수를 선언한다.
name := '홍길동' // 문자열 '홍길동'을 name이라는 상자에 넣는다
age := 25 // 정수 25를 age라는 상자에 넣는다
pi := 3.14 // 실수 3.14를 pi라는 상자에 넣는다
잠깐, 다른 언어에서는 int age = 25;처럼 타입을 직접 적어야 했는데, V에서는 타입을 전혀 쓰지 않았다. 어떻게 된 걸까?
타입 추론 — V가 알아서 상자 크기를 정해준다
V는 타입 추론(type inference) 을 지원한다. := 오른쪽에 넣은 값을 보고, V 컴파일러가 "아, 이건 정수니까 int 타입이군"이라고 자동으로 판단해준다.
x := 42 // V가 보기에 42는 정수 → x의 타입은 int
y := 3.14 // 3.14는 소수점이 있으니까 → y의 타입은 f64
z := true // true/false는 → z의 타입은 bool
s := '안녕' // 따옴표로 감싸면 → s의 타입은 string
상자에 넣는 물건을 보고 "이건 책이니까 책꽂이 크기의 상자를 주자"라고 결정해주는 셈이다. 덕분에 코드가 매우 간결해진다.
물론, 원한다면 타입을 직접 지정할 수도 있다. 특히 기본 추론과 다른 타입을 사용하고 싶을 때 유용하다.
small_number := i16(42) // int가 아니라 i16(16비트 정수)으로 지정
big_float := f32(3.14) // f64가 아니라 f32(32비트 실수)로 지정
🔍 다른 언어와 비교:
언어 변수 선언 방식 타입 추론 V x := 42✅ Go x := 42✅ Python x = 42✅ C int x = 42;❌ Rust let x = 42;✅ V의
:=문법은 Go와 거의 동일하다. Python보다 한 글자(:) 더 쓰지만, 그 한 글자 덕분에 "이 줄에서 새 변수를 만든다" 는 의도가 확실해진다.
변수 이름 짓기 규칙
V의 변수 이름에는 몇 가지 규칙이 있다.
// ✅ 올바른 이름
user_name := '홍길동' // snake_case (V가 권장하는 스타일)
count := 10
is_valid := true
// ❌ 잘못된 이름
// UserName := '홍길동' // 대문자로 시작하면 안 된다 (V에서 대문자 시작은 특별한 용도)
// 1st_name := '홍길동' // 숫자로 시작할 수 없다
V는 변수와 함수에 snake_case(소문자 + 밑줄)를 사용한다. userName(camelCase)이 아니라 user_name으로 쓴다. 이 규칙 덕분에 누가 작성하든 같은 스타일의 코드가 나온다.
불변이 기본! — mut 키워드
V에서 가장 중요한 개념 중 하나가 바로 이것이다.
V의 변수는 기본적으로 바꿀 수 없다(immutable).
무슨 말인지 코드로 보자.
name := '홍길동'
name = '김철수' // ❌ 컴파일 에러! 불변 변수는 값을 바꿀 수 없다
위 코드를 실행하면 컴파일러가 에러를 낸다. name은 한 번 값을 넣으면 절대 바꿀 수 없는 변수이기 때문이다.
값을 바꾸고 싶다면, 변수 앞에 mut(mutable의 줄임말)이라는 키워드를 붙여서 "이 변수는 바꿀 수 있어" 라고 명시해야 한다.
mut name := '홍길동' // mut을 붙여서 '변경 가능'하다고 선언
name = '김철수' // ✅ 이제 변경 가능!
println(name) // 김철수
왜 이렇게 귀찮게 설계했을까?
"그냥 다 바꿀 수 있게 하면 편하지 않나?"라고 생각할 수 있다. V가 mut 을 명시하도록 강제하는 데는 이유가 있다.
1. 버그를 줄여준다. 프로그램이 커지면, 어딘가에서 의도치 않게 변수 값이 바뀌어 버그가 생기는 경우가 매우 많다. 기본이 불변이면 "실수로 값을 바꾸는" 일 자체가 불가능해진다.
2. 코드를 읽기 쉽게 만든다. mut이 붙은 변수만 찾으면 "이 변수가 코드 어딘가에서 변할 것이다"라고 예측할 수 있다. mut이 없는 변수는? 처음 넣은 값 그대로라는 보장이 있으므로 안심하고 읽을 수 있다.
3. 안전한 동시성 프로그래밍. 여러 작업이 동시에 같은 변수에 접근할 때, 값이 함부로 바뀌지 않으면 훨씬 안전하다. (10편 동시성에서 자세히 다룬다.)
💡 비유하자면: 기본이 불변이라는 것은 "택배 상자에 테이프를 한 번 봉하면 뜯을 수 없는 것"과 비슷하다. 만약 뜯어야 할 상자라면, 미리 "개봉 가능" 스티커(
mut)를 붙여두어야 한다. 귀찮아 보이지만, 실수로 잘못 뜯는 사고를 막아준다.
불변과 가변 한눈에 정리
// 불변 (기본값) — 안전하고 예측 가능
score := 100
// score = 200 // ❌ 에러
// 가변 — mut을 붙이면 변경 가능
mut level := 1
level = 2 // ✅ OK
level = 3 // ✅ OK
println(level) // 3
기본 타입 하나씩 소개
V가 제공하는 기본 데이터 타입을 하나씩 살펴보자. 프로그래밍에서 타입이란, 상자에 담을 수 있는 물건의 종류를 정해놓은 것이다.
정수 타입 (int)
정수(integer) 는 소수점이 없는 숫자다. 1, 42, -7 같은 것들이다.
age := 25 // int (기본 정수 타입)
temperature := -10 // 음수도 가능
hex := 0xff // 16진수 표기 (255)
oct := 0o77 // 8진수 표기 (63)
bin := 0b1111_0000 // 2진수 표기 (240)
million := 1_000_000 // 밑줄로 자릿수 구분 (읽기 편하게)
마지막 줄을 보자. 1_000_000은 1000000과 같은 값이지만 밑줄(_)로 자릿수를 끊어서 한눈에 "100만"이라고 읽을 수 있다. 작은 배려이지만 실제 코드를 읽을 때 매우 유용하다.
정수 타입의 크기 변형
int는 기본 정수 타입이지만, 필요에 따라 더 작거나 큰 정수 타입을 선택할 수 있다.
| 타입 | 크기 | 범위 | 용도 예시 |
|---|---|---|---|
i8 | 8비트 | -128 ~ 127 | 매우 작은 수 |
i16 | 16비트 | -32,768 ~ 32,767 | 작은 수 |
int | 32비트 | -21억 ~ 21억 | 기본, 대부분 이걸 쓴다 |
i64 | 64비트 | 약 -922경 ~ 922경 | 아주 큰 수 (타임스탬프 등) |
u8 | 8비트 | 0 ~ 255 | 바이트 데이터 |
u16 | 16비트 | 0 ~ 65,535 | 포트 번호 등 |
u32 | 32비트 | 0 ~ 약 42억 | 양수만 다룰 때 |
u64 | 64비트 | 0 ~ 약 1,844경 | 매우 큰 양수 |
i는 integer(정수), u는 unsigned(부호 없는, 즉 양수만) 의 줄임말이다. 뒤의 숫자는 차지하는 메모리 크기(비트 수)를 나타낸다.
대부분의 경우 그냥 int를 쓰면 된다. 특별히 큰 수를 다루거나, 메모리를 아껴야 하는 상황에서만 다른 타입을 선택하면 된다.
byte_val := u8(255) // 0~255 범위의 바이트 값
big_number := i64(9_999_999_999_999) // int 범위를 넘는 큰 수
실수 타입 (f32, f64)
실수(floating-point number) 는 소수점이 있는 숫자다. 3.14, 0.5, -2.7 같은 것들이다.
pi := 3.14159 // f64 (기본 실수 타입 — 64비트)
half := f32(0.5) // f32 (32비트 — 메모리 절약이 필요할 때)
negative := -2.7 // 음수 실수도 가능
| 타입 | 크기 | 정밀도 | 설명 |
|---|---|---|---|
f32 | 32비트 | 약 7자리 | 메모리 절약 / 게임 등 |
f64 | 64비트 | 약 15자리 | 기본, 대부분 이걸 쓴다 |
V에서 소수점이 있는 숫자를 쓰면 기본으로 f64가 된다. f32를 쓰고 싶으면 f32(값) 형태로 명시하면 된다.
💡 왜 두 가지가 있을까?
f64는 더 정밀하지만 메모리를 두 배로 쓴다. 수백만 개의 점(좌표)을 다루는 게임이나 3D 프로그램에서는f32로 메모리를 아끼고, 금융 계산처럼 정밀도가 중요한 경우에는f64를 쓴다.
불리언 타입 (bool)
불리언(boolean) 은 참(true) 또는 거짓(false), 단 두 가지 값만 가질 수 있는 타입이다. "이 질문에 대한 대답이 예/아니오인가?"를 표현할 때 쓴다.
is_admin := true // 관리자인가? → 참
is_logged_in := false // 로그인했는가? → 거짓
can_vote := age >= 18 // 18세 이상인가? → age 값에 따라 true 또는 false
bool은 if 조건문과 함께 가장 많이 쓰인다. (조건문은 4편에서 자세히 다룬다.)
is_adult := true
if is_adult {
println('성인입니다')
} else {
println('미성년자입니다')
}
문자열 타입 (string)
문자열(string) 은 텍스트 데이터를 담는 타입이다. 작은따옴표('...')로 감싸서 만든다.
greeting := '안녕하세요!'
name := '홍길동'
empty := '' // 빈 문자열도 가능
V의 문자열은 불변(immutable) 이다. 한 번 만들어진 문자열의 개별 글자를 바꿀 수는 없다. 새로운 문자열을 만들어야 한다.
mut s := 'hello'
// s[0] = `H` // ❌ 에러! 문자열의 개별 문자는 변경 불가
s = 'Hello' // ✅ 새 문자열을 통째로 대입하는 건 가능 (mut이니까)
룬 타입 (rune)
룬(rune) 은 하나의 유니코드 문자를 나타내는 타입이다. 백틱(`)으로 감싼다.
letter := `A` // 영문 한 글자
korean := `가` // 한글 한 글자
emoji := `🎉` // 이모지도 하나의 rune
rune은 문자열(string)과 다르다. 문자열은 여러 글자의 묶음이고, rune은 정확히 한 글자만 담는다.
🔍 다른 언어와 비교:
- Go:
rune타입이 동일하게 존재한다 (유니코드 코드 포인트)- Python: 별도 문자 타입이 없다 (한 글자도
str)- C:
char가 있지만 1바이트 — 한글이나 이모지를 담을 수 없다V의
rune은 Go와 마찬가지로 유니코드를 완전히 지원하므로, 한글·이모지 등 어떤 문자든 하나의rune에 담을 수 있다.
문자열 다루기
문자열은 프로그래밍에서 가장 자주 쓰이는 타입이다. V가 제공하는 편리한 문자열 기능들을 알아보자.
문자열 보간 (String Interpolation)
1편의 "직접 해보기"에서 이미 맛봤던 ${} 문법이다. 문자열 안에 변수 값을 끼워넣을 수 있다.
name := '홍길동'
age := 25
// 기본 보간 — ${변수명}
println('이름: ${name}') // 이름: 홍길동
// 수식도 가능 — ${표현식}
println('내년 나이: ${age + 1}') // 내년 나이: 26
// 메서드 호출도 가능
println('대문자: ${name.len}글자') // 대문자: 3글자
${} 안에는 단순한 변수뿐 아니라 계산식이나 함수 호출도 넣을 수 있다. 문자열을 조합하기 위해 +로 이어 붙이는 것보다 훨씬 읽기 쉽다.
💡 주의: V에서는
'${name}'처럼 작은따옴표 안에${}를 사용한다. 큰따옴표("...")가 아니라 작은따옴표라는 점을 기억하자. (V에서 큰따옴표는 쓸 수 없다 — 작은따옴표만 사용한다.)
문자열 연결
+ 연산자로 두 문자열을 이어 붙일 수 있다.
first := '홍'
last := '길동'
full_name := first + last // '홍길동'
println(full_name)
// += 으로 뒤에 추가하기 (mut 변수만 가능)
mut message := '안녕'
message += '하세요' // '안녕하세요'
message += '!' // '안녕하세요!'
println(message)
여러 줄 문자열
긴 텍스트를 여러 줄에 걸쳐 쓰고 싶을 때는 '...' 그대로 줄 바꿈을 넣으면 된다.
poem := '산에는 꽃 피네
꽃이 피네
갈 봄 여름 없이
꽃이 피네'
println(poem)
Raw 문자열
특수 문자를 있는 그대로 표현하고 싶을 때 r'...'(raw string)을 사용한다.
// 일반 문자열
path := 'C:\\Users\\홍길동' // \\를 써야 \가 하나 출력된다
// raw 문자열 — r을 앞에 붙이면 이스케이프 처리를 안 한다
raw_path := r'C:\Users\홍길동' // \를 그냥 쓸 수 있다!
println(raw_path) // C:\Users\홍길동
Windows 파일 경로나 정규식처럼 역슬래시(\)를 많이 쓰는 상황에서 raw 문자열이 매우 편리하다.
문자열 인덱싱과 슬라이싱
문자열의 특정 위치에 있는 글자를 꺼내거나, 일부분을 잘라낼 수 있다.
s := 'hello'
// 인덱싱 — 특정 위치의 바이트를 가져온다 (0부터 시작)
first := s[0] // u8(104) → ASCII 코드 'h'
println(first == `h`) // true
// 슬라이싱 — 범위를 잘라서 새 문자열로 만든다
slice := s[1..4] // 인덱스 1부터 3까지 → 'ell'
println(slice)
head := s[..3] // 처음부터 인덱스 2까지 → 'hel'
tail := s[2..] // 인덱스 2부터 끝까지 → 'llo'
⚠️ 참고: 인덱싱(
s[0])은 바이트 단위로 접근한다. 영어 알파벳은 1바이트이므로 문제없지만, 한글은 한 글자가 3바이트를 차지한다. 한글 문자열을 다룰 때는runes()메서드를 사용하는 것이 안전하다.
타입 변환
V는 암묵적(implicit) 타입 변환을 허용하지 않는다. 즉, 정수와 실수를 아무 생각 없이 섞어 쓸 수 없다.
x := 42 // int
// y := x + 3.14 // ❌ 에러! int와 f64를 바로 더할 수 없다
y := f64(x) + 3.14 // ✅ x를 f64로 변환 후 더하기
println(y) // 45.14
이것이 V의 안전성 철학이다. "어? 정수에 실수를 더했더니 소수점이 잘렸네..."같은 실수를 원천 차단하는 것이다.
변환 방법
타입 변환은 타입명(값) 형태로 쓴다. 마치 함수를 호출하는 것처럼 생겼다.
// 정수 → 실수
a := 42
b := f64(a) // 42.0
// 실수 → 정수 (소수점 이하 버림)
c := 3.99
d := int(c) // 3 (반올림이 아니라 버림!)
// 정수 크기 변환
e := 1000
f := i16(e) // 1000 (i16 범위 안이므로 OK)
// 정수 → 문자열
g := 42
h := g.str() // '42'
// 문자열 → 정수
i := '123'
j := i.int() // 123
💡
int(3.99)가3인 이유: V는 실수를 정수로 변환할 때 소수점 이하를 버린다(절삭, truncation). 반올림이 아니다!3.99든3.01이든 결과는3이다. 이 점을 기억해두자.
상수 (const)
상수(constant) 는 한 번 정해지면 프로그램이 끝날 때까지 절대 바뀌지 않는 값이다. const 키워드로 선언한다.
const max_score = 100
const pi = 3.14159
const app_name = 'My V App'
"어? 변수도 기본이 불변이라고 했는데, 상수와 뭐가 다르지?"라는 의문이 들 수 있다.
변수(불변) vs 상수 — 차이점
| 구분 | 불변 변수 (x := 42) | 상수 (const x = 42) |
|---|---|---|
| 선언 위치 | 함수 안 | 함수 밖 (모듈 레벨) |
| 값 결정 시점 | 프로그램 실행 중 | 컴파일 시점 |
mut 가능 여부 | ✅ (mut x := 42) | ❌ 절대 불가 |
| 용도 | 일반적인 데이터 저장 | 프로그램 전체에서 쓰는 고정 값 |
핵심 차이는 선언 위치와 값 결정 시점이다. 상수는 함수 밖에 선언하고, 컴파일할 때 이미 값이 확정된다. 따라서 "이 프로그램에서 π는 언제나 3.14159"같은 보편적인 값에 상수를 쓴다.
const max_retries = 3
const base_url = 'https://api.example.com'
fn main() {
// 상수는 어디서든 읽을 수 있다
println('최대 재시도 횟수: ${max_retries}')
println('API 주소: ${base_url}')
// max_retries = 5 // ❌ 에러! 상수는 변경 불가
}
여러 상수를 한꺼번에 선언
관련 있는 상수들은 const (...) 블록으로 묶어서 깔끔하게 선언할 수 있다.
const (
width = 800
height = 600
title = 'V Game'
)
fn main() {
println('화면 크기: ${width} x ${height}')
println('제목: ${title}')
}
📝 정리
이번 글에서 배운 핵심 포인트를 체크리스트로 정리한다.
- [x]
:=로 변수 선언 — 타입은 V가 자동 추론, 직접 지정도 가능 (i64(값)) - [x] 불변이 기본 — 변수는 기본적으로 변경 불가,
mut키워드로 가변 선언 - [x] 기본 타입 6가지 —
int,f64,bool,string,rune+ 크기별 변형 (i8~i64,u8~u64,f32) - [x] 문자열 보간 —
'${변수}'로 문자열 안에 값 삽입, 수식도 가능 - [x] 암묵적 타입 변환 없음 —
f64(x)처럼 명시적으로 변환해야 안전 - [x] 상수(
const) — 함수 밖에서 선언, 컴파일 시점에 확정, 절대 변경 불가
🧪 직접 해보기
아래 과제를 직접 시도해보자.
과제 1: 자기소개 카드
아래 실행 결과가 나오도록 변수와 문자열 보간을 사용해 프로그램을 만들어보자.
=== 자기소개 ===
이름: (본인 이름)
나이: (본인 나이)
취미: (본인 취미)
V 언어 학습 시작: 2026년
힌트: name, age, hobby, year 네 개의 변수를 만들고 println에서 ${}으로 보간하면 된다.
과제 2: 타입 변환 실험
아래 코드를 작성하고 실행한 뒤, 출력 결과를 예측해보자. 예측과 실제 결과가 같은가?
fn main() {
a := 7
b := 2
c := f64(a) / f64(b)
d := a / b
println('실수 나눗셈: ${c}') // 예측해보자: ?
println('정수 나눗셈: ${d}') // 예측해보자: ?
e := int(9.99)
println('int(9.99) = ${e}') // 예측해보자: ?
}
💡 힌트: 정수끼리 나누면 결과도 정수가 된다 (소수점 버림).
int()로 변환할 때도 소수점은 버림이다.
다음 편 예고
3편: V 언어 문법 — 함수 정의부터 클로저까지
코드를 재사용 가능한 블록으로 나누는 함수를 배운다. 매개변수, 반환값, 여러 값 동시 반환, 그리고 "이름 없는 함수"인 클로저까지 — 3편에서 함수의 모든 것을 만나보자.