V 언어 문법: 조건문, 반복문, 패턴 매칭
지금까지 배운 프로그램은 위에서 아래로 한 줄씩 순서대로 실행됐다. 하지만 현실 세계의 프로그램은 그렇게 단순하지 않다. "로그인했으면 대시보드를, 아니면 로그인 화면을 보여줘", "장바구니의 모든 상품에 대해 가격을 합산해" 같은 로직이 필요하다. 이번 편에서는 프로그램에 판단력과 반복 능력을 부여하는 방법을 배운다.
if / else — 조건에 따라 다르게 행동하기
if 는 프로그래밍에서 가장 기본적인 제어 구문이다. "만약 ~라면 이것을 해라, 아니라면 저것을 해라"를 표현한다.
기본 형태
age := 20
if age >= 19 {
println('성인입니다')
} else {
println('미성년자입니다')
}
구조를 뜯어보자.
if age >= 19 {
│ ──────────
│ └─ 조건: true 또는 false로 평가되는 표현식
└──────── if 키워드
println('성인입니다') ← 조건이 true일 때 실행
} else {
println('미성년자입니다') ← 조건이 false일 때 실행
}
핵심 규칙 하나: V에서는 조건에 괄호를 쓰지 않는다. if (age >= 19)이 아니라 if age >= 19이다. C/Java에서 넘어온 분들은 이 점에 주의하자.
else if로 여러 조건 체크
score := 85
if score >= 90 {
println('A등급')
} else if score >= 80 {
println('B등급')
} else if score >= 70 {
println('C등급')
} else {
println('F등급')
}
// 출력: B등급
위에서부터 순서대로 조건을 검사하다가, 처음으로 true가 되는 블록이 실행된다. score가 85이면 첫 번째 조건(>= 90)은 거짓이므로 넘어가고, 두 번째 조건(>= 80)이 참이므로 "B등급"이 출력된다. 나머지 조건은 아예 검사하지 않는다.
표현식으로서의 if — 값을 반환하는 if
V에서 if는 단순한 문(statement)이 아니라 표현식(expression) 이다. 즉, if 자체가 값을 돌려줄 수 있다. 이것은 매우 강력한 기능이다.
age := 20
status := if age >= 19 { '성인' } else { '미성년자' }
println(status) // 성인
다른 언어에서 삼항 연산자(? :) 를 쓰는 자리에 V는 if 표현식을 쓴다.
// 다른 언어의 삼항 연산자 (V에는 없다):
// status = age >= 19 ? "성인" : "미성년자"
// V의 방식: if 표현식
status := if age >= 19 { '성인' } else { '미성년자' }
V에는 삼항 연산자(? :)가 없다. 대신 if 표현식이 그 역할을 완벽하게 대체한다. 한 줄로 쓸 수 있으면서도 if/else라는 익숙한 문법이므로 더 읽기 쉽다는 것이 V의 설계 의도다.
if 표현식을 사용할 때 주의할 점이 하나 있다. else 블록이 반드시 있어야 한다. 값을 반환하는 용도이므로, 모든 경우에 값이 있어야 하기 때문이다.
// ✅ OK — else가 있으므로 모든 경우에 값이 있다
max := if a > b { a } else { b }
// ❌ 에러 — else가 없으면 false일 때 값이 없다
// max := if a > b { a }
실전 예제: 최댓값 함수
fn max(a int, b int) int {
return if a > b { a } else { b }
}
fn main() {
println(max(10, 20)) // 20
println(max(99, 3)) // 99
}
if 표현식을 return에 바로 사용할 수 있어서 코드가 매우 간결해진다.
match — 강력한 패턴 매칭
match는 다른 언어의 switch에 해당하지만, 훨씬 더 강력하다. 하나의 값을 여러 경우와 비교해서 일치하는 블록을 실행한다.
기본 사용법
day := 'Monday'
match day {
'Monday' { println('월요일 — 한 주의 시작!') }
'Friday' { println('금요일 — 불금!') }
'Saturday', 'Sunday' { println('주말 — 쉬는 날!') }
else { println('평범한 평일') }
}
핵심 특징을 정리하면 이렇다.
1. break가 필요 없다. C/Java의 switch에서는 각 case 끝에 break를 빠뜨리면 다음 케이스로 넘어가는 (fall-through) 버그가 생긴다. V의 match는 일치하는 블록 하나만 실행하고 자동으로 빠져나온다.
2. 여러 값을 쉼표로 묶을 수 있다. 'Saturday', 'Sunday'처럼 쉼표로 구분하면 "둘 중 하나라도 일치하면"이라는 뜻이다.
3. else는 "그 밖의 모든 경우"를 처리한다. C의 default에 해당한다.
match도 표현식이다
if처럼 match도 값을 돌려줄 수 있다.
day := 'Wednesday'
korean_day := match day {
'Monday' { '월요일' }
'Tuesday' { '화요일' }
'Wednesday' { '수요일' }
'Thursday' { '목요일' }
'Friday' { '금요일' }
'Saturday' { '토요일' }
'Sunday' { '일요일' }
else { '알 수 없음' }
}
println(korean_day) // 수요일
숫자 범위와 match
score := 85
grade := match true {
score >= 90 { 'A' }
score >= 80 { 'B' }
score >= 70 { 'C' }
else { 'F' }
}
println('${score}점 → ${grade}등급') // 85점 → B등급
match true는 "아래 조건 중 처음으로 참인 것을 찾아라"라는 뜻이다. if/else if 체인과 같은 역할을 하면서도 더 깔끔하게 쓸 수 있다.
모든 경우를 빠짐없이 처리 (Exhaustive Matching)
V의 match가 switch보다 강력한 또 다른 이유는, 모든 경우를 빠짐없이 처리하도록 강제한다는 점이다. 특히 열거형(enum)과 함께 쓸 때 빛을 발한다. (열거형은 7편에서 다룬다.)
enum Season {
spring
summer
autumn
winter
}
fn describe(s Season) string {
return match s {
.spring { '꽃이 피는 계절' }
.summer { '더운 계절' }
.autumn { '단풍의 계절' }
.winter { '눈이 오는 계절' }
// else가 없어도 된다 — 모든 경우를 다 적었기 때문
}
}
코드에 처음 보는 문법이 두 가지 등장했다. 하나씩 짚어보자.
match s는 무엇인가? 앞서 본 match day처럼, match 뒤에 검사할 변수를 적는다. match s는 "변수 s의 값을 아래 항목들과 하나씩 대조하라" 는 뜻이다. s가 Season 타입이므로, spring, summer, autumn, winter 중 어떤 값인지를 확인하게 된다.
.spring의 점(.)은 무엇인가? 원래대로라면 Season.spring이라고 적어야 한다. 하지만 V 컴파일러는 match s에서 s가 Season 타입이라는 것을 이미 알고 있기 때문에, 타입 이름을 생략하고 .spring처럼 점(.)만 찍어도 된다. 즉 .spring은 Season.spring의 줄임 표현이다.
// 이 두 줄은 동일하다
.spring { '꽃이 피는 계절' } // 줄임 표현 (V가 타입을 알아서 추론)
Season.spring { '꽃이 피는 계절' } // 전체 표현
이 줄임 문법 덕분에 match 블록이 깔끔해진다. 열거형(enum)에 대해서는 7편에서 본격적으로 다루니, 지금은 ".값 = 타입 이름 생략" 정도만 기억해두자.
만약 여기서 .winter 케이스를 빼먹으면? 컴파일러가 "winter 케이스가 누락됐습니다" 라고 에러를 낸다. 덕분에 "이 경우를 깜빡 잊었네" 같은 실수를 사전에 방지할 수 있다.
🔍 다른 언어와 비교:
특징 V matchC/Java switchPython match(3.10+)fall-through ❌ (안전) ⚠️ break 필요 ❌ 표현식으로 사용 ✅ ❌ ❌ 빠짐없는 검사 ✅ ❌ ❌ 여러 값 묶기 ✅ (쉼표) ✅ (case 연속) ✅ ( \|)
for 루프 — 반복의 모든 것
프로그래밍에서 반복(loop) 은 "같은 작업을 여러 번 수행"하는 것이다. V는 반복문으로 for 하나만 사용한다. while이나 do-while 같은 키워드가 별도로 없다. 대신 for의 형태를 바꿔서 모든 반복 패턴을 표현한다.
형태 1: C 스타일 for
초기값, 조건, 증감을 한 줄에 쓰는 전통적인 형태다.
for i := 0; i < 5; i++ {
println(i)
}
// 0, 1, 2, 3, 4
구조를 분해하면 이렇다.
for i := 0; i < 5; i++ {
─────── ────── ────
초기값 조건 증감
(처음 1번) (매번 검사) (매번 실행)
i := 0— 시작할 때i를 0으로 만든다 (한 번만 실행)i < 5— 매 반복 전에 검사한다. 거짓이면 루프를 빠져나온다i++— 매 반복이 끝날 때마다i를 1 증가시킨다
형태 2: 조건만 있는 for (다른 언어의 while)
조건만 적으면 while 루프처럼 동작한다.
mut count := 0
for count < 3 {
println('count: ${count}')
count++
}
// count: 0
// count: 1
// count: 2
V에는 while 키워드가 없다. for 조건 { ... }이 그 역할을 한다. 키워드 하나로 통일해서 배울 것을 줄인 것이다.
형태 3: 범위 for (for i in 0..n)
가장 자주 쓰는, 가장 깔끔한 형태다.
for i in 0 .. 5 {
println(i)
}
// 0, 1, 2, 3, 4
0 .. 5는 "0 이상 5 미만"이라는 범위(range) 를 나타낸다. 시작은 포함하고 끝은 포함하지 않는다.
// 1부터 10까지 합
mut sum := 0
for n in 1 .. 11 { // 1, 2, 3, ..., 10 (11은 미포함)
sum += n
}
println('1~10의 합: ${sum}') // 1~10의 합: 55
💡 왜 끝값을 포함하지 않을까?
0 .. 5가 0~4인 이유는, 이렇게 하면 범위의 길이 = 끝값 - 시작값이 되어 계산이 편하기 때문이다.0 .. 5는 총 5개(5 - 0 = 5)의 수를 순회한다. 대부분의 현대 언어가 이 방식을 채택한다.
형태 4: 무한 루프 (for { ... })
조건을 아예 생략하면 무한 루프가 된다.
mut i := 0
for {
if i >= 3 {
break // 탈출 조건을 직접 작성
}
println(i)
i++
}
// 0, 1, 2
break를 만나면 루프를 즉시 빠져나온다. 무한 루프는 서버나 게임 루프처럼 "종료 조건을 나중에 판단"하는 경우에 쓰인다.
네 가지 형태 한눈에 정리
| 형태 | 문법 | 다른 언어 대응 | 사용 상황 |
|---|---|---|---|
| C 스타일 | for i := 0; i < n; i++ | C/Java의 for | 인덱스가 필요할 때 |
| 조건만 | for condition | while | 반복 횟수를 모를 때 |
| 범위 | for i in 0..n | Python의 range | 가장 자주 쓴다 |
| 무한 | for { ... } | while (true) | 서버, 게임 루프 |
for ... in — 컬렉션 순회
for ... in은 배열, 문자열, 맵 같은 컬렉션(여러 데이터의 묶음) 을 하나씩 꺼내면서 반복하는 형태다. 5편에서 배열과 맵을 본격적으로 다루지만, 순회 문법은 여기서 미리 익혀두자.
배열 순회
fruits := ['사과', '바나나', '딸기']
for fruit in fruits {
println('과일: ${fruit}')
}
// 과일: 사과
// 과일: 바나나
// 과일: 딸기
인덱스와 값을 동시에 받기
순서 번호(인덱스)가 필요하면 두 개의 변수로 받는다.
fruits := ['사과', '바나나', '딸기']
for i, fruit in fruits {
println('${i}번째: ${fruit}')
}
// 0번째: 사과
// 1번째: 바나나
// 2번째: 딸기
i에는 0부터 시작하는 인덱스가, fruit에는 실제 값이 들어간다.
문자열 순회
문자열을 for ... in으로 순회하면 각 바이트를 하나씩 꺼낸다.
word := 'hello'
for i, ch in word {
println('${i}: ${ch}')
}
// 0: h
// 1: e
// 2: l
// 3: l
// 4: o
맵(Map) 순회
맵의 키와 값을 동시에 꺼낼 수 있다.
ages := {
'홍길동': 25
'김영희': 30
}
for name, age in ages {
println('${name}: ${age}살')
}
// 홍길동: 25살
// 김영희: 30살
in 연산자 — 포함 여부 확인
in 연산자는 "이 값이 저 안에 있는가?" 를 검사한다. 결과는 true 또는 false다.
배열에서의 in
colors := ['빨강', '파랑', '초록']
if '빨강' in colors {
println('빨간색이 목록에 있다!') // 이것이 출력된다
}
if '노랑' !in colors {
println('노란색은 목록에 없다!') // 이것도 출력된다
}
!in은 in의 반대, 즉 "포함되지 않는가"를 검사한다.
범위에서의 in
age := 25
if age in 19 .. 30 {
println('20대입니다') // 이것이 출력된다
}
score := 85
if score in 80 .. 90 {
println('B등급 범위입니다') // 이것도 출력된다
}
19 .. 30은 19 이상 30 미만의 범위를 나타낸다. in과 함께 쓰면 "이 값이 이 범위 안에 있는가?"를 간결하게 표현할 수 있다.
break와 continue
루프의 흐름을 중간에 바꿀 수 있는 두 가지 키워드가 있다.
break — 루프 탈출
break는 루프를 즉시 중단하고 빠져나간다.
for i in 0 .. 100 {
if i == 5 {
break // i가 5가 되면 루프 종료
}
println(i)
}
// 0, 1, 2, 3, 4 (5에서 멈추므로 5는 출력되지 않는다)
continue — 현재 반복 건너뛰기
continue는 현재 반복의 나머지 코드를 건너뛰고 다음 반복으로 넘어간다.
for i in 0 .. 6 {
if i == 3 {
continue // i가 3일 때는 println을 건너뛴다
}
println(i)
}
// 0, 1, 2, 4, 5 (3만 빠졌다)
💡 비유:
break는 "이 수업 끝! 집에 가자"이고,continue는 "이 문제는 건너뛰고 다음 문제로 가자"이다.
레이블(Label) — 중첩 루프 탈출
루프 안에 루프가 있는 중첩 루프에서, 안쪽 루프에서 바깥 루프까지 한 번에 빠져나가고 싶을 때 레이블을 사용한다.
outer: for i in 0 .. 5 {
for j in 0 .. 5 {
if i + j == 4 {
println('i=${i}, j=${j}에서 탈출!')
break outer // 바깥 루프까지 한 번에 탈출
}
}
}
// i=0, j=4에서 탈출!
outer:가 레이블이다. break outer는 "outer라는 이름이 붙은 루프를 탈출하라"는 뜻이다. 레이블이 없으면 break는 가장 안쪽 루프만 빠져나간다.
continue에도 레이블을 쓸 수 있다.
outer: for i in 0 .. 3 {
for j in 0 .. 3 {
if j == 1 {
continue outer // 바깥 루프의 다음 반복으로 건너뛴다
}
println('i=${i}, j=${j}')
}
}
// i=0, j=0
// i=1, j=0
// i=2, j=0
// (j가 1이 되면 바깥 루프로 돌아가므로 j=1, j=2는 출력되지 않는다)
if unwrapping — Optional 값 안전하게 꺼내기
이 부분은 8편(에러 처리) 에서 본격적으로 다루지만, 제어 흐름의 맥락에서 미리 맛만 보자.
V에서 실패할 수 있는 함수는 Option 타입(?타입) 이나 Result 타입(!타입) 을 반환한다. 이 값을 안전하게 꺼내는 패턴이 if unwrapping이다.
// 배열에서 첫 번째 요소를 가져오는 예제
arr := [1, 2, 3]
// arr[10]처럼 범위 밖을 접근하면 에러 가능 → #[]로 안전 접근
if val := arr[0] {
println('첫 번째 값: ${val}') // 첫 번째 값: 1
} else {
println('값을 가져올 수 없다')
}
if val := arr[0] 형태는 "값을 꺼내는 데 성공하면 val에 넣고, 실패하면 else 블록으로 가라"는 뜻이다. 8편에서 더 자세히 다루니 지금은 "이런 패턴이 있구나" 정도로 기억해두면 된다.
📝 정리
이번 글에서 배운 핵심 포인트를 체크리스트로 정리한다.
- [x]
if/else— 조건에 따른 분기, 괄호 없이 사용 - [x]
if표현식 —if가 값을 반환할 수 있다 (삼항 연산자 대체) - [x]
match— 여러 값과 비교하는 패턴 매칭,break불필요, 빠짐없는 검사 강제 - [x]
for4가지 형태 — C 스타일, 조건만, 범위(0..n), 무한 루프 - [x]
for ... in— 배열/문자열/맵 순회, 인덱스+값 동시 받기 - [x]
in연산자 — 배열이나 범위에 값이 포함되는지 확인 (!in으로 반대) - [x]
break/continue— 루프 중단 / 현재 반복 건너뛰기, 레이블로 중첩 루프 제어
🧪 직접 해보기
과제 1: FizzBuzz
프로그래밍 면접의 고전 문제인 FizzBuzz를 V로 작성해보자.
- 1부터 30까지 숫자를 출력한다
- 3의 배수이면 숫자 대신 "Fizz" 출력
- 5의 배수이면 숫자 대신 "Buzz" 출력
- 3과 5의 공배수이면 "FizzBuzz" 출력
fn main() {
for i in 1 .. 31 {
// 여기를 완성하세요!
// 힌트: 나머지 연산자 %를 사용
// 힌트: 15의 배수(3과 5의 공배수)를 먼저 검사해야 한다
}
}
과제 2: 구구단 출력기
match를 활용해서 사용자가 원하는 단의 구구단을 출력하는 프로그램을 만들어보자.
fn print_times_table(dan int) {
// 여기를 완성하세요!
// dan이 1~9 범위인지 확인
// 범위 안이면 구구단 출력
// 범위 밖이면 에러 메시지 출력
}
fn main() {
print_times_table(7)
// 출력:
// 7 x 1 = 7
// 7 x 2 = 14
// ...
// 7 x 9 = 63
}
다음 편 예고
5편: V 언어 문법 — 배열과 맵, 컬렉션 다루기
여러 개의 데이터를 한꺼번에 다루는 배열과 맵을 배운다. 선언, 슬라이싱, 그리고
map,filter,sort같은 강력한 내장 메서드까지 — 데이터를 다루는 힘이 폭발적으로 늘어나는 5편을 기대하자.