V 언어 문법: 모듈 시스템과 패키지 관리
지금까지 모든 코드를 하나의 파일에 작성했다. 간단한 프로그램이라면 괜찮지만, 코드가 수백, 수천 줄로 늘어나면 하나의 파일에 모든 것을 담는 건 악몽이 된다. 이번 편에서는 코드를 깔끔하게 나누고 정리하는 도구인 모듈 시스템을 배운다.
왜 모듈이 필요한가?
프로그램이 커지면 이런 문제가 생긴다.
- 한 파일이 너무 길어진다 — 3,000줄짜리 파일에서 원하는 함수를 찾기가 어렵다
- 이름이 충돌한다 — 다른 기능인데 같은 이름의 함수를 만들고 싶다 (
calc의add와string의add) - 재사용이 어렵다 — 유용한 함수를 다른 프로젝트에서도 쓰고 싶은데, 파일 하나에 다 섞여 있다
비유하자면 서랍장 정리와 같다. 옷장에 양말, 셔츠, 바지를 한 칸에 다 넣으면 찾기 힘들다. 칸을 나눠서 양말은 양말 칸, 셔츠는 셔츠 칸에 넣으면 훨씬 편하다. 모듈은 코드의 서랍 칸이다.
모듈 만들기 — 폴더가 곧 모듈
V에서는 폴더 하나가 모듈 하나다. 매우 단순한 규칙이다.
첫 번째 모듈 만들기
아래와 같은 폴더 구조를 만들어보자.
my-project/
├── main.v # 진입점
└── math_util/ # 모듈 폴더
└── add.v # 모듈의 소스 파일
1단계: 모듈 파일 작성
math_util/add.v 파일을 만들고, 맨 위에 module 키워드로 이 파일이 어떤 모듈에 속하는지 선언한다.
// math_util/add.v
module math_util
pub fn add(a int, b int) int {
return a + b
}
pub fn subtract(a int, b int) int {
return a - b
}
핵심 포인트 두 가지를 짚어보자.
module math_util — 이 파일은 math_util 모듈에 속한다는 선언이다. 모듈 이름은 폴더 이름과 같아야 한다.
pub fn — 함수 앞에 pub이 붙었다. 이것은 "이 함수를 외부(다른 모듈)에서도 사용할 수 있게 공개한다"는 뜻이다. pub이 없으면 같은 모듈 안에서만 사용 가능하다. 6편에서 구조체 필드의 pub을 배웠는데, 같은 원리다.
2단계: 모듈 사용하기
main.v에서 import로 모듈을 가져와 사용한다.
// main.v
import math_util
fn main() {
result := math_util.add(3, 5)
println(result) // 8
diff := math_util.subtract(10, 4)
println(diff) // 6
}
import math_util — math_util 모듈을 가져온다. 이후 math_util.add()처럼 모듈이름.함수이름 형태로 호출한다.
모듈 규칙 정리
| 규칙 | 설명 |
|---|---|
| 폴더 이름 = 모듈 이름 | math_util/ 폴더 → module math_util |
| 모듈 이름은 snake_case | math_util ✅, MathUtil ❌ |
한 폴더에 여러 .v 파일 가능 | 같은 module 선언을 공유 |
pub 없으면 외부 사용 불가 | 모듈 내부 전용 함수/타입/상수 |
pub 키워드 — 현관문 열기
pub은 모듈 시스템에서 가장 중요한 키워드다. "외부에 공개할 것만 골라서 열어주는 현관문" 이라고 생각하면 된다.
pub이 적용되는 대상
pub은 함수뿐 아니라 구조체, 상수, 열거형에도 붙일 수 있다.
// utils/helpers.v
module utils
// ✅ 외부에서 사용 가능
pub const max_retries = 3
pub struct Config {
pub:
host string
port int
}
pub enum LogLevel {
debug
info
warning
error_level
}
pub fn format_name(first string, last string) string {
return '${last} ${first}'
}
// ❌ 외부에서 사용 불가 — pub이 없다
fn internal_helper() string {
return '내부 전용'
}
// main.v
import utils
fn main() {
println(utils.max_retries) // 3 ✅
println(utils.format_name('길동', '홍')) // 홍 길동 ✅
c := utils.Config{ host: 'localhost', port: 8080 }
println(c.host) // localhost ✅
level := utils.LogLevel.info
println(level) // LogLevel.info ✅
// utils.internal_helper() // ❌ 컴파일 에러! pub이 아닌 함수
}
왜 기본이 비공개일까?
"다 공개하면 편한데 왜 숨기나?"라고 생각할 수 있다. 이유는 두 가지다.
1. 내부 구현을 숨긴다. 외부에 공개하지 않은 함수는 언제든 자유롭게 바꿀 수 있다. "이 함수의 이름을 바꿔야겠다" → pub이 아니면 그냥 바꾸면 된다. pub이면 외부에서 쓰고 있을 수 있으므로 쉽게 바꿀 수 없다.
2. 사용자에게 명확한 인터페이스를 제공한다. pub이 붙은 것들만이 "이 모듈의 사용법"이다. 사용자는 pub 함수 목록만 보면 되고, 내부 구현 세부 사항은 알 필요 없다.
💡 비유: 식당에서 손님에게 보여주는 것은 메뉴판(
pub) 이다. 주방 레시피(내부 함수)는 보여주지 않는다. 메뉴판만 깔끔하면, 주방에서 레시피를 바꿔도 손님은 영향받지 않는다.
import의 다양한 형태
기본 import
import math_util
fn main() {
math_util.add(1, 2)
}
별칭(alias) import
모듈 이름이 길거나, 이름이 겹칠 때 별칭을 붙일 수 있다.
import math_util as mu
fn main() {
mu.add(1, 2) // math_util 대신 mu로 사용
}
선택적 import
특정 함수/타입만 골라서 가져올 수 있다. 이 경우 모듈 이름 없이 바로 사용한다.
import math_util { add, subtract }
fn main() {
add(1, 2) // math_util. 없이 바로 사용
subtract(5, 3)
}
여러 모듈 import
import math_util
import os
import strings
import 방식 비교
| 방식 | 코드 | 호출 | 적합한 상황 |
|---|---|---|---|
| 기본 | import math_util | math_util.add() | 일반적 |
| 별칭 | import math_util as mu | mu.add() | 이름이 길 때 |
| 선택적 | import math_util { add } | add() | 자주 쓰는 함수가 적을 때 |
🔍 다른 언어와 비교:
언어 import 방식 V import math_util { add }Go import "math_util"(선택적 없음)Python from math_util import addRust use math_util::add;Java import math_util.add;V의 선택적 import는 Python의
from ... import ...와 매우 유사하다.
모듈 디렉토리 구조
실제 프로젝트에서 모듈을 어떻게 배치하는지 살펴보자.
기본 구조
my-project/
├── main.v # 진입점 (module main)
├── v.mod # 프로젝트 정보 파일
├── config/ # config 모듈
│ ├── config.v # module config
│ └── parser.v # module config (같은 모듈)
├── models/ # models 모듈
│ └── user.v # module models
└── utils/ # utils 모듈
└── helpers.v # module utils
핵심 규칙을 정리하면 이렇다.
1. 한 폴더의 모든 .v 파일은 같은 모듈이다. config/ 폴더에 config.v와 parser.v가 있으면, 둘 다 module config이다. 마치 한 서랍에 여러 물건을 넣는 것과 같다.
2. 진입점(main.v)은 module main이다. 하지만 main.v에서는 module main을 생략해도 된다. V가 자동으로 처리한다.
3. 한 폴더에 서로 다른 모듈을 섞을 수 없다. config/ 폴더 안에 module utils라고 적으면 에러가 난다.
한 모듈에 여러 파일 — 코드 분리
모듈 하나가 커지면, 같은 폴더 안에서 파일을 나눌 수 있다. 같은 module 선언을 공유하기 때문에 서로의 함수를 자유롭게 호출할 수 있다.
math_util/
├── add.v # module math_util — 덧셈/뺄셈 함수
├── multiply.v # module math_util — 곱셈/나눗셈 함수
└── constants.v # module math_util — 상수 정의
// math_util/add.v
module math_util
pub fn add(a int, b int) int {
return a + b
}
// math_util/multiply.v
module math_util
pub fn multiply(a int, b int) int {
return a * b
}
fn internal_square(n int) int { // pub 없음 — 모듈 내부 전용
return n * n
}
// math_util/constants.v
module math_util
pub const pi = 3.14159
pub const e = 2.71828
세 파일 모두 module math_util이므로 하나의 모듈이다. multiply.v에서 add.v의 add 함수를, constants.v의 pi 상수를 별도의 import 없이 바로 사용할 수 있다.
init 함수 — 모듈 초기화
V 모듈에는 init 이라는 특별한 함수가 있다. 모듈이 처음 로드될 때 자동으로 한 번 실행되는 초기화 함수다.
// database/db.v
module database
mut connection := ''
fn init() {
connection = 'connected to database'
println('DB 모듈 초기화 완료')
}
pub fn get_connection() string {
return connection
}
// main.v
import database
fn main() {
// database 모듈을 import하는 순간 init()이 자동 실행된다
// 출력: DB 모듈 초기화 완료
println(database.get_connection()) // connected to database
}
init 함수의 특징을 정리하면 이렇다.
| 특징 | 설명 |
|---|---|
| 직접 호출하지 않는다 | import 시 자동 실행 |
| 매개변수/반환값이 없다 | fn init() 고정 형태 |
| 모듈당 하나 | 여러 파일에 걸쳐 정의 불가 |
| 실행 시점 | main() 함수보다 먼저 실행 |
💡 주의:
init은 설정 로드, 리소스 초기화 등 꼭 필요한 초기화에만 사용하자.init에 복잡한 로직을 넣으면 디버깅이 어려워진다.
순환 의존 금지
V에서는 순환 의존(circular dependency) 이 금지되어 있다. A 모듈이 B를 사용하고, B 모듈이 다시 A를 사용하는 것은 불가능하다.
❌ 순환 의존 — 컴파일 에러!
┌──────┐ ┌──────┐
│ A │─────→│ B │
│ │←─────│ │
└──────┘ └──────┘
A가 B를 import, B가 A를 import
✅ 단방향 의존 — OK
┌──────┐ ┌──────┐
│ A │─────→│ B │
│ │ │ │
└──────┘ └──────┘
A만 B를 import
왜 금지할까?
순환 의존은 "닭이 먼저냐 달걀이 먼저냐" 문제를 일으킨다. A를 컴파일하려면 B가 필요하고, B를 컴파일하려면 A가 필요한데 — 둘 다 아직 없으니 컴파일이 불가능하다.
순환 의존이 생기면?
보통 설계를 다시 생각해야 한다는 신호다. 해결 방법은 두 가지다.
1. 공통 모듈로 추출: A와 B가 서로 필요로 하는 부분을 C라는 새 모듈로 빼낸다.
✅ 공통 모듈 추출
┌──────┐ ┌──────┐
│ A │─────→│ C │←─────│ B │
└──────┘ └──────┘ └──────┘
A와 B 모두 C만 의존
2. 인터페이스로 의존 역전: 7편에서 배운 인터페이스를 활용해서 의존 방향을 바꾼다.
V 패키지 매니저
V 생태계의 외부 라이브러리를 v install 명령어로 설치하고 사용할 수 있다.
v.mod 파일
프로젝트 루트에 v.mod 파일을 만들어 프로젝트 정보를 기록한다.
// v.mod
Module {
name: 'my_project'
description: '나의 첫 V 프로젝트'
version: '0.1.0'
dependencies: []
}
| 필드 | 설명 |
|---|---|
name | 프로젝트(모듈) 이름 |
description | 프로젝트 설명 |
version | 버전 (시맨틱 버저닝) |
dependencies | 외부 라이브러리 목록 |
🔍 다른 언어와 비교:
언어 프로젝트 설정 파일 패키지 설치 명령 V v.modv installGo go.modgo getPython pyproject.tomlpip installRust Cargo.tomlcargo addNode.js package.jsonnpm install
외부 패키지 설치
# VPM(V Package Manager)에서 패키지 설치
v install nedpals.args
# GitHub에서 직접 설치
v install https://github.com/user/repo
설치한 패키지는 일반 모듈처럼 import해서 사용한다.
import nedpals.args
fn main() {
// 패키지의 pub 함수 사용
}
V 표준 라이브러리
V는 자주 쓰는 기능을 표준 라이브러리로 제공한다. 별도 설치 없이 import만 하면 된다.
import os // 파일 시스템, 환경변수
import strings // 문자열 유틸리티
import math // 수학 함수
import json // JSON 파싱
import net.http // HTTP 요청
import time // 시간 관련
몇 가지 예를 살펴보자.
import os
fn main() {
// 현재 디렉토리의 파일 목록
files := os.ls('.') or {
println('에러: ${err}')
return
}
for f in files {
println(f)
}
}
import math
fn main() {
println(math.sqrt(16.0)) // 4.0
println(math.pi) // 3.141592653589793
println(math.pow(2, 10)) // 1024.0
}
8편에서 배운 에러 처리가 여기서 빛을 발한다. os.ls()는 ![]string을 반환하므로 or {} 블록으로 에러를 처리한다.
전체 프로젝트 예제
지금까지 배운 모듈 개념을 모두 활용한 작은 프로젝트를 구성해보자.
todo-app/
├── main.v
├── v.mod
├── models/
│ └── task.v
└── utils/
└── formatter.v
// v.mod
Module {
name: 'todo_app'
description: 'V로 만든 할일 관리 앱'
version: '0.1.0'
dependencies: []
}
// models/task.v
module models
pub struct Task {
pub:
title string
completed bool
}
pub fn (t Task) status() string {
return if t.completed { '✅' } else { '⬜' }
}
pub fn new_task(title string) Task {
return Task{ title: title, completed: false }
}
// utils/formatter.v
module utils
pub fn header(text string) string {
line := '─'.repeat(text.len + 4)
return '${line}\n ${text}\n${line}'
}
// main.v
import models
import utils
fn main() {
println(utils.header('할일 목록'))
tasks := [
models.new_task('V 언어 9편 읽기'),
models.Task{ title: 'V 프로젝트 만들기', completed: true },
models.new_task('10편 예습하기'),
]
for i, task in tasks {
println('${i + 1}. ${task.status()} ${task.title}')
}
}
출력:
──────────────
할일 목록
──────────────
1. ⬜ V 언어 9편 읽기
2. ✅ V 프로젝트 만들기
3. ⬜ 10편 예습하기
models 모듈에서 데이터(Task)를, utils 모듈에서 표시 유틸리티를 가져와 main에서 조립했다. 각 모듈은 자기 역할에만 집중하고 있다.
📝 정리
이번 글에서 배운 핵심 포인트를 체크리스트로 정리한다.
- [x] 폴더 = 모듈 — 폴더 이름이 모듈 이름,
module 이름선언 필수 - [x]
pub— 외부에 공개할 함수/타입/상수에 붙이기, 기본은 비공개 - [x]
import— 기본import 모듈, 별칭as, 선택적{ 함수명 } - [x] 다중 파일 모듈 — 같은 폴더의
.v파일들은 같은 모듈, import 없이 상호 접근 - [x]
init함수 — 모듈 로드 시 자동 실행,main()보다 먼저 - [x] 순환 의존 금지 — A→B→A 불가, 공통 모듈 추출로 해결
- [x]
v.mod— 프로젝트 정보와 의존성 기록,v install로 패키지 설치 - [x] 표준 라이브러리 —
os,math,json,net.http등 바로 사용 가능
🧪 직접 해보기
과제 1: 계산기 모듈
아래 구조로 간단한 계산기 프로젝트를 만들어보자.
calculator/
├── main.v
└── calc/
└── operations.v
calc 모듈에 add, subtract, multiply, divide 함수를 만들고, main.v에서 불러와 사용해보자. divide는 8편에서 배운 Result 타입(!f64)으로 0 나누기를 처리하자.
과제 2: 표준 라이브러리 탐험
os와 math 모듈을 import해서 아래를 구현해보자.
import os
import math
fn main() {
// 1) 현재 작업 디렉토리 출력 (os.getwd())
// 2) 원주율 출력 (math.pi)
// 3) 2의 10승 계산 (math.pow(2, 10))
// 4) 환경변수 HOME (또는 USERPROFILE) 읽기 (os.getenv())
}
다음 편 예고
10편: V 언어 문법 — 동시성 프로그래밍과 채널
시리즈의 마지막 편! 여러 작업을 동시에 처리하는
spawn, 스레드 간 데이터를 주고받는 채널(channel),select문, 공유 객체까지 — V의 동시성 프로그래밍으로 성능을 끌어올려보자.