V 언어 문법: 모듈 시스템과 패키지 관리

V 언어 문법: 모듈 시스템과 패키지 관리

지금까지 모든 코드를 하나의 파일에 작성했다. 간단한 프로그램이라면 괜찮지만, 코드가 수백, 수천 줄로 늘어나면 하나의 파일에 모든 것을 담는 건 악몽이 된다. 이번 편에서는 코드를 깔끔하게 나누고 정리하는 도구인 모듈 시스템을 배운다.

왜 모듈이 필요한가?

프로그램이 커지면 이런 문제가 생긴다.

  • 한 파일이 너무 길어진다 — 3,000줄짜리 파일에서 원하는 함수를 찾기가 어렵다
  • 이름이 충돌한다 — 다른 기능인데 같은 이름의 함수를 만들고 싶다 (calcaddstringadd)
  • 재사용이 어렵다 — 유용한 함수를 다른 프로젝트에서도 쓰고 싶은데, 파일 하나에 다 섞여 있다

비유하자면 서랍장 정리와 같다. 옷장에 양말, 셔츠, 바지를 한 칸에 다 넣으면 찾기 힘들다. 칸을 나눠서 양말은 양말 칸, 셔츠는 셔츠 칸에 넣으면 훨씬 편하다. 모듈은 코드의 서랍 칸이다.


모듈 만들기 — 폴더가 곧 모듈

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_utilmath_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 add
Rust 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.vparser.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.vadd 함수를, constants.vpi 상수를 별도의 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.mod v install
Go go.mod go get
Python pyproject.toml pip install
Rust Cargo.toml cargo add
Node.js package.json npm 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: 표준 라이브러리 탐험

osmath 모듈을 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의 동시성 프로그래밍으로 성능을 끌어올려보자.