도입: 스택 머신이라는 익숙한 설명
WebAssembly(WASM)를 처음 배울 때 누구나 듣는 설명이 있어요. "WASM은 스택 기반 가상 머신이다." 그래서 명령어들이 값을 스택에 푸시하고, 연산은 스택 위의 값들을 꺼내서 계산하고 결과를 다시 푸시한다고요. 예를 들어 i32.add라는 명령은 스택에서 두 개의 i32 값을 꺼내서 더한 뒤 결과를 스택에 다시 올려놓는 식이죠. JVM이나 .NET CLR과 비슷한 모델이라고 이해하면 편해요.
그런데 이번에 공유된 글은 이 익숙한 설명에 한 마디 덧붙여요. "WASM은 스택 머신이긴 한데, 우리가 흔히 생각하는 그런 자유로운 스택 머신은 아니다." 이게 무슨 말인지가 글의 핵심이에요. 이 미묘한 차이를 알면 WASM을 타겟으로 하는 컴파일러를 짜거나, WASM 코드를 디버깅할 때 훨씬 깊이 이해할 수 있어요.
핵심 내용: 무엇이 진짜 스택 머신과 다른가
진짜 스택 머신은 보통 "스택을 자유롭게 다룰 수 있는" 머신이에요. 어디서든 값을 푸시하고 팝하고, 분기점을 만나도 스택 상태가 어떻게 되든 상관없이 합쳐질 수 있어요. 그런데 WASM은 그렇지 않아요. WASM은 타입 검사와 구조적 제어 흐름이라는 두 가지 강한 제약을 두거든요.
첫째, 타입이 정적으로 정해져 있어요. 모든 명령어 위치에서 스택 위에 어떤 타입의 값들이 어떤 순서로 쌓여 있어야 하는지가 컴파일 타임에 결정되어 있어야 해요. i32.add를 호출하려면 그 시점에 스택 맨 위에 i32 두 개가 정확히 있어야 하고, 그 사실이 검증기에 의해 미리 체크돼요. 동적 타입 스택은 허용되지 않아요.
둘째, 제어 흐름이 구조적이에요. 일반적인 어셈블리는 임의의 위치로 점프(goto)할 수 있는데, WASM은 그렇지 않아요. block, loop, if 같은 구조화된 블록 안에서만 분기가 가능하고, br이나 br_if는 자기를 둘러싼 블록을 빠져나가거나 루프 시작점으로 돌아가는 식으로만 동작해요. 그리고 결정적인 게, 블록의 시작과 끝에서 스택 상태가 정확히 일치해야 해요. 블록에 들어갈 때 스택에 뭐가 있었는지, 나갈 때 뭐가 남아 있어야 하는지가 블록의 시그니처로 명시되어 있어야 하거든요.
이게 컴파일러를 짜는 사람 입장에서는 꽤 큰 제약이에요. 일반적인 IR(중간 표현)에서 WASM으로 코드를 내릴 때, 자유롭게 점프하는 CFG(제어 흐름 그래프)를 구조화된 형태로 변환해야 하거든요. 이걸 CFG의 구조화(structurization) 또는 relooper, stackifier 알고리즘이라고 해요. Emscripten 초기에는 알론 자카이가 만든 Relooper 알고리즘이 유명했고, 이후 Binaryen에는 더 발전된 버전이 들어가 있어요.
또 한 가지 미묘한 점은, 스택 자체를 "임시 변수"처럼 자유롭게 못 쓴다는 거예요. 예를 들어 "이 값을 잠깐 스택에 올려두고 다른 일 하다가 나중에 쓰자"가 안 돼요. 왜냐면 분기를 가로질러서 스택 위에 값을 들고 갈 때마다, 모든 분기 경로의 스택 상태가 정확히 일치해야 하기 때문이에요. 그래서 실제로는 임시 값들을 로컬 변수(local) 에 저장하고 필요할 때 다시 꺼내는 방식으로 컴파일하는 경우가 많아요. 결국 WASM은 "표현식 트리를 후위 표기법으로 늘어놓은 형태"에 더 가깝고, 진짜로 자유롭게 굴러가는 스택 머신이라기보다 구조화된 식 평가기에 가까워요.
업계 맥락: 왜 이렇게 설계됐을까
WASM이 이런 제약을 둔 데에는 분명한 이유가 있어요. 검증 속도와 안전성 때문이에요. 브라우저는 인터넷에서 받아온 코드가 메모리를 망가뜨리지 않는다는 걸 빠르게 확인해야 하잖아요? 타입과 제어 흐름이 구조적으로 명세되어 있으면, 단일 패스로 선형 시간에 검증이 가능해요. JVM은 더 자유로운 대신 검증이 더 복잡해요. 또 구조화된 제어 흐름은 JIT/AOT 컴파일러가 빠르게 최적화하기에도 유리해요. 루프와 블록 경계가 명시적이니까 분석이 쉽거든요.
비슷한 결정을 한 다른 IR로는 MLIR이나 Cranelift IR 같은 게 있어요. Cranelift는 WASM을 빠르게 네이티브로 변환하기 위해 만들어진 컴파일러인데, 자기 IR도 구조화된 형태에 가까워요. 반대로 LLVM IR은 자유로운 CFG를 쓰니까 LLVM에서 WASM으로 내려갈 때 별도의 구조화 단계가 필요해요.
한국 개발자에게 주는 시사점
WASM을 직접 컴파일러로 다룰 일이 없더라도, 이 차이를 아는 건 의미 있어요. 예를 들어 Rust나 Go 코드를 WASM으로 빌드해서 브라우저에서 돌릴 때, 왜 어떤 패턴은 잘 최적화되고 어떤 건 그렇지 않은지를 이해하는 데 도움이 돼요. 또 WASM 바이트코드를 직접 들여다봐야 할 디버깅 상황이라면, 로컬 변수가 왜 그렇게 많이 생기는지, 왜 비슷한 코드가 반복되는지 이해할 수 있죠.
마무리
한 줄로 정리하면, WASM은 "스택 머신"이라는 라벨보다 훨씬 정교하고 제약이 많은 구조화된 가상 머신이에요. 여러분이 WASM 타겟으로 컴파일했을 때 의외로 코드가 커지거나 이상한 형태가 나왔던 경험이 있다면, 아마 이 제약들 때문이었을 거예요. WASM의 다음 진화(WASM GC, exception handling)는 이 모델을 또 어떻게 바꿀까요?
🔗 출처: Hacker News
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공