Gleam 완벽 가이드 | BEAM VM·정적 타입·패턴 매칭·OTP·Wisp 웹 서버
이 글의 핵심
Gleam의 설계 철학, 대수적 타입과 exhaustive 패턴 매칭, Result로 표현하는 오류, Erlang/Elixir와의 FFI, gleam_otp로 구성하는 감독 트리, Wisp와 Mist로 만드는 HTTP 서버까지 한 흐름으로 정리한 고급 가이드입니다.
이 글의 핵심
Gleam은 Erlang 가상 머신(BEAM)과 JavaScript를 타깃으로 하는 정적 타입 함수형 언어입니다. 문법은 Rust·ML 계열에 가깝고, 런타임 특성은 Erlang/Elixir와 같습니다. 이 글에서는 Gleam만의 타입 시스템, 패턴 매칭, Result 기반 오류 모델, Erlang·Elixir와의 상호운용, gleam_otp를 통한 OTP 스타일 병렬·내결함성, 그리고 Wisp와 Mist로 HTTP 서버를 띄우는 실전 흐름까지 연결해 설명합니다.
선행 지식: 함수형 언어의 기본(불변 데이터, 일급 함수), HTTP·프로세스 개념, Erlang 생태계 이름(OTP, BEAM)에 대한 대략적인 이해가 있으면 읽기 수월합니다.
1. Gleam이란 무엇인가
1.1 설계 목표
Gleam은 (1) 컴파일 타임에 많은 실수를 잡을 것, (2) BEAM의 프로세스·슈퍼비전·핫 코드 로딩 같은 강점을 그대로 쓸 것, (3) 도구와 언어 규칙을 단순하게 유지할 것을 목표로 합니다. 그 결과 언어 차원에서 null을 두지 않고 Option으로, 실패 가능한 연산은 Result로 표현하는 쪽을 권장합니다.
1.2 왜 BEAM인가
BEAM은 대량의 경량 프로세스, 선점형 스케줄링, 프로세스 간 메시지 전달, 슈퍼비전 트리를 전제로 한 런타임입니다. Gleam은 Erlang 타깃으로 컴파일될 때 이 모델을 그대로 사용하므로, 동시성·분산·장애 복구를 언어 외부 라이브러리가 아니라 런타임과 OTP 패턴으로 다루기 좋습니다.
1.3 JavaScript 타깃
동일한 Gleam 코드가 JavaScript로도 컴파일될 수 있어, 프런트엔드·서버리스·Node 환경과 코드·타입을 공유하는 전략이 가능합니다. 다만 이 글의 초점은 BEAM에서의 실행과 OTP·Erlang 상호운용에 맞춥니다.
2. 도구 체인과 프로젝트 생성
2.1 설치
공식 사이트의 안내에 따라 Gleam 컴파일러와 Erlang/OTP(또는 필요 시 Node)를 설치합니다. 설치 후 터미널에서 gleam --version으로 확인합니다.
2.2 새 프로젝트
gleam new my_app
cd my_app
gleam run # 기본 엔트리 실행
gleam.toml에 의존성을 추가하고 gleam build로 빌드합니다. 패키지는 Hex와 packages.gleam.run에서 검색하는 것이 일반적입니다.
3. 핵심 개념
3.1 모듈과 가시성
각 파일은 하나의 모듈에 대응하며, pub로 내보낸 항목만 외부에 공개됩니다. API 경계를 명시적으로 두어 리팩터링과 캡슐화에 유리합니다.
// math.gleam
pub fn add(a: Int, b: Int) -> Int {
a + b
}
fn double(x: Int) -> Int {
x * 2
}
double은 같은 모듈 안에서만 쓰이도록 제한됩니다. 공개 여부를 타입 수준에서 나누는 습관은 대규모 코드베이스에서 특히 도움이 됩니다.
3.2 불변 데이터와 흐름
Gleam의 기본 자료 구조는 불변입니다. “객체를 고친다”기보다 새 값을 만든다는 표현이 자연스럽습니다. 상태 변경이 필요하면 새 바인딩이나 Actor 같은 동시성 추상을 사용합니다.
3.3 파이프 연산자
|>는 앞 식의 결과를 다음 함수의 첫 인자로 넘깁니다. 중첩 호출을 줄이고 데이터가 흐르는 순서를 읽기 쉽게 만듭니다.
import gleam/int
import gleam/string
pub fn example(x: String) -> String {
x
|> string.trim
|> string.reverse
}
3.4 함수와 타입 어노테이션
인자와 반환에 타입을 붙이며, 제네릭과 고차 함수를 자연스럽게 씁니다.
pub fn map_list(items: List(a), f: fn(a) -> b) -> List(b) {
case items {
[] -> []
[first, ..rest] -> [f(first), ..map_list(rest, f)]
}
}
4. 타입 시스템
4.1 기본 타입과 별칭
Int, Float, Bool, String, BitArray, UtfCodepoint 등이 있으며, type으로 별칭을 만들 수 있습니다.
pub type UserId =
Int
4.2 대수적 데이터 타입(Algebraic Data Type)
type으로 여러 변형(variant)을 정의합니다. 각 변형은 자신만의 필드를 가질 수 있습니다.
pub type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
}
pub fn area(shape: Shape) -> Float {
case shape {
Circle(radius: r) -> 3.141592 *. r *. r
Rectangle(width: w, height: h) -> w *. h
}
}
컴파일러는 case가 모든 변형을 다루는지 검사하는 경향이 있어, 새 변형을 추가하면 누락된 분기를 컴파일 오류로 알려 줍니다.
4.3 제네릭과 옵션·결과
표준 라이브러리의 Option과 Result는 제네릭으로 정의되어 있습니다.
import gleam/option.{type Option}
import gleam/result.{type Result}
// Option: 값이 있거나 없거나
// Result: 성공(Ok) 또는 실패(Error)
null이 없으므로 “없음”은 Option.None으로, 실패는 Result.Error로 표현하는 것이 관례입니다.
4.4 불투명 타입(Opaque)
모듈 밖에서는 내부 표현을 숨기고 생성자만 공개해 불변식을 지킬 수 있습니다. API가 커질수록 실수로 내부 구조에 의존하는 코드를 줄이는 데 유용합니다.
5. 패턴 매칭
5.1 case와 구조 분해
case는 표현식이며, 모든 분기가 같은 타입의 값을 내야 합니다. 리스트는 [head, ..tail] 형태로 꼬리를 분해합니다.
import gleam/int
pub fn describe_list(xs: List(Int)) -> String {
case xs {
[] -> "empty"
[x] -> "single: " <> int.to_string(x)
[a, b, ..] ->
"at least two, first diff: " <> int.to_string(a - b)
}
}
5.2 가드와 중첩
패턴에 추가 조건을 붙일 수 있으며, 중첩된 자료 구조를 한 번에 분해할 수 있습니다. 이는 도메인 모델이 Result와 엮일 때 특히 유용합니다.
5.3 라우팅에 패턴 매칭 쓰기
Wisp는 별도 DSL 라우터보다 경로 세그먼트를 패턴 매칭하는 방식을 권장합니다. 분기가 명시적이고, 컴파일러와 리뷰어가 흐름을 따라가기 쉽습니다. 아래는 공식 Wisp 저장소의 routing 예제에서 발췌·요약한 형태입니다.
import gleam/http.{Get, Post}
import wisp.{type Request, type Response}
pub fn handle_request(req: Request) -> Response {
case wisp.path_segments(req) {
[] -> home_page(req)
["comments"] -> comments(req)
["comments", id] -> show_comment(req, id)
_ -> wisp.not_found()
}
}
fn home_page(req: Request) -> Response {
use <- wisp.require_method(req, Get)
wisp.ok()
|> wisp.html_body("Hello, Joe!")
}
fn comments(req: Request) -> Response {
case req.method {
Get -> list_comments()
Post -> create_comment(req)
_ -> wisp.method_not_allowed([Get, Post])
}
}
fn show_comment(req: Request, id: String) -> Response {
use <- wisp.require_method(req, Get)
wisp.ok()
|> wisp.html_body("Comment with id " <> id)
}
wisp.path_segments로 경로를 리스트로 바꾼 뒤, 빈 경로·고정 세그먼트·동적 id를 한눈에 매칭합니다. HTTP 메서드는 req.method에 대해 또 한 번 case로 나눕니다.
6. 에러 처리: Result와 use
6.1 실패를 타입으로
함수가 실패할 수 있으면 반환 타입을 Result(성공, 오류)로 두는 편이 안전합니다. 호출자는 case나 gleam/result의 조합자로 처리합니다.
import gleam/int
import gleam/result
pub fn parse_positive(s: String) -> Result(Int, String) {
case int.parse(s) {
Error(_) -> Error("not a number")
Ok(n) if n > 0 -> Ok(n)
Ok(_) -> Error("must be positive")
}
}
6.2 use와 연속 실패
여러 단계가 모두 Result를 반환할 때, use 문법으로 “성공 시에만 다음 줄로 진행”하는 스타일을 쓸 수 있습니다. 콜백이 중첩되는 것보다 읽기 쉽습니다.
의미론적으로는 “앞 단계가 Ok이면 값을 풀어 다음 블록에 넘기고, Error이면 그대로 반환”에 가깝습니다. 팀 내에서 오류 타입을 하나로 통일해 두면 체인이 길어져도 추적이 수월합니다.
6.3 panic과 assert
디버깅·불가능한 상태용으로 panic이나 let assert가 있으나, 라이브러리 경계와 사용자 입력 처리에는 Result를 우선하는 것이 좋습니다. “도달하면 안 되는 분기”에만 제한적으로 사용하십시오.
7. Erlang·Elixir 상호운용
7.1 컴파일 결과
Erlang 타깃으로 빌드하면 .beam 파일이 생성되고, Erlang 모듈 이름 규칙에 맞게 호출할 수 있습니다. 반대로 Erlang·Elixir에서 생성한 모듈도 Gleam 코드에서 외부 함수로 연결할 수 있습니다.
7.2 @external로 FFI 선언
Gleam은 @external 속성으로 특정 타깃에서의 구현을 지정합니다. Erlang 모듈의 함수를 직접 매핑할 때 흔히 씁니다.
@external(erlang, "erlang", "monotonic_time")
pub fn monotonic_time() -> Int
첫 번째 인자는 타깃(erlang 또는 javascript), 다음은 Erlang 모듈 이름과 함수 이름입니다. 인자·반환 타입은 실제 호출과 일치해야 하며, 불일치 시 런타임에서 깨지기 쉽습니다. 래퍼 모듈을 두고 좁은 API만 노출하는 편이 안전합니다.
7.3 Elixir와의 공존
같은 OTP 애플리케이션 안에 Elixir 앱과 Gleam 앱을 함께 둘 수 있습니다. 메시지 전달·프로세스 사전·ETS 등 BEAM 공유 자원을 그대로 사용합니다. 경계에서는 데이터 표현(튜플, 맵, 구조체)을 명확히 문서화하는 것이 중요합니다.
7.4 gleam_erlang
프로세스 스폰, 포트, 타이머 등 Erlang 런타임과의 접점은 gleam_erlang 등 패키지로 감싼 경우가 많습니다. 직접 @external을 늘리기 전에 기존 바인딩이 있는지 확인하는 것이 유지보수에 유리합니다.
8. OTP와 병렬성
8.1 프로세스와 액터
BEAM의 프로세스는 OS 스레드와 다릅니다. Gleam에서도 액터 모델을 따르며, gleam_otp는 Actor, Supervisor, Task 등 OTP와 유사한 추상을 제공합니다. 장애가 발생하면 슈퍼바이저 정책에 따라 자식을 재시작하고, 나머지 트리는 계속 살아 있게 설계합니다.
8.2 감독 전략
일반적인 전략은 다음과 같습니다.
- OneForOne: 실패한 자식만 재시작
- OneForAll: 한 자식이 죽으면 형제 전부 종료 후 전부 재시작
- RestForOne: 실패한 자식과 그보다 뒤에 시작된 자식만 재시작
웹 서버와 워커 풀, 캐시 프로세스를 나눌 때 트리를 그리고 각 노드의 재시작 의미를 정하는 것이 운영의 기준이 됩니다.
8.3 Gleam에서의 실무 접근
OTP 전체를 이 글에서 구현할 수는 없지만, 방향은 명확합니다. 순수 로직은 Gleam 함수로, 프로세스 경계와 재시작 정책은 gleam_otp와 슈퍼바이저로 나눕니다. 순수 함수는 테스트하기 쉽고, 액터는 동시성·상태·장애 복구를 담당합니다.
9. 실전 웹 서버: Wisp와 Mist
9.1 역할 분담
- Wisp: 요청·응답, 미들웨어, JSON·폼·쿠키·정적 파일 등 웹 애플리케이션 관례를 제공합니다.
- Mist: 실제 TCP/HTTP 수신과 연결 관리를 담당합니다.
Wisp 공식 문서와 예제는 HexDocs의 wisp와 GitHub 예제 디렉터리를 참고하십시오.
9.2 의존성 예시
프로젝트에 따라 버전 범위는 조정합니다.
# gleam.toml (예시)
[dependencies]
gleam_stdlib = ">= 0.50.0 and < 2.0.0"
wisp = ">= 1.0.0"
mist = ">= 2.0.0"
wisp_mist = ">= 1.0.0"
gleam_erlang = ">= 1.0.0 and < 2.0.0"
9.3 엔트리포인트: 로거, 시크릿, Mist 기동
아래는 Wisp 저장소의 hello_world 예제 app.gleam을 인용한 것입니다. 운영에서는 secret_key_base를 환경 변수나 시크릿 저장소에서 읽어야 합니다.
import gleam/erlang/process
import hello_world/app/router
import mist
import wisp
import wisp_mist
pub fn main() {
wisp.configure_logger()
let secret_key_base = wisp.random_string(64)
let assert Ok(_) =
wisp_mist.handler(router.handle_request, secret_key_base)
|> mist.new
|> mist.port(8000)
|> mist.start
process.sleep_forever()
}
wisp_mist.handler가 라우터 함수와 시크릿을 받아 Mist 서버 설정으로 연결합니다. 서버는 별도 Erlang 프로세스에서 동작하므로, 메인은 process.sleep_forever()로 종료되지 않게 둡니다.
9.4 라우터와 HTML 응답
같은 예제의 라우터는 미들웨어 스택을 적용한 뒤 HTML 본문을 돌려줍니다.
import hello_world/app/web
import wisp.{type Request, type Response}
pub fn handle_request(req: Request) -> Response {
use _req <- web.middleware(req)
let body = " Hello, Joe! "
wisp.html_response(body, 200)
}
web.middleware는 프로젝트별로 로깅·보안 헤더·바디 크기 제한 등을 묶는 곳입니다. JSON API라면 wisp.require_json과 gleam/json을 조합하는 패턴이 문서화되어 있습니다.
9.5 테스트 가능성
Wisp는 핸들러를 일반 Gleam 함수로 두기 때문에, HTTP 요청 객체를 조립해 단위 테스트하기 쉽습니다. 실제 네트워크 없이 응답 코드와 본문을 검증할 수 있으면 리팩터링 비용이 줄어듭니다.
10. 모범 사례와 주의점
10.1 오류 타입을 팀 규칙으로
Result의 오류 쪽을 문자열로만 두면 시간이 지나 분기가 흐트러집니다. 커스텀 타입으로 오류 원인을 나누고, 로깅·HTTP 상태 코드 매핑을 한곳에 모으는 편이 좋습니다.
10.2 FFI 경계 최소화
@external을 남발하면 타입 검사를 빠져나간 코드가 늘어납니다. 얇은 래퍼 모듈로 감싸고, 나머지는 Gleam으로 작성하십시오.
10.3 OTP 설계는 “트리부터”
프로세스를 필요할 때마다 추가하면 감독 구조가 엉킵니다. 먼저 어떤 실패가 격리되어야 하는지를 정하고 슈퍼바이저 트리를 그린 뒤, 그에 맞춰 액터를 배치합니다.
10.4 웹 레이어는 얇게
데이터베이스·도메인 규칙은 순수 모듈에 두고, Wisp 핸들러는 파싱·권한·응답 코드 매핑에 집중하는 구조가 테스트와 변경에 유리합니다.
11. 정리
Gleam은 정적 타입과 함수형 관례로 BEAM의 강점을 구조적으로 활용하려는 언어입니다. 대수적 타입과 exhaustive에 가까운 패턴 매칭으로 모델을 안전하게 다루고, Result로 실패를 명시하며, @external과 OTP·프로세스로 Erlang·Elixir 생태계와 맞닿습니다. 웹에서는 Wisp가 애플리케이션 패턴을, Mist가 전송 계층을 맡는 구성이 널리 쓰입니다. 새 서비스를 BEAM 위에 올리거나 Elixir 코드베이스에 타입 안전한 코어를 붙일 때 Gleam은 설계·운영 모두에서 설득력 있는 선택지가 됩니다.