안녕하세요! 그로스로그 입니다!😊 🌱
계산을 서버에서 브라우저로, 지금에서 나중으로, 메인 스레드에서 옆 스레드로. 옮긴 자리가 다른 세 편입니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.
2회차에는 WebAssembly · Kotlin Sequence · Web Worker가 올라왔습니다. 언어도 플랫폼도 다른데, 묶어놓고 보니 같은 질문을 다루고 있었어요.
01
🧪 나만의 온라인 C 에디터 만들기
오세명 님
학교 C 프로그래밍 수업을 들으며 브라우저에서 C 코드를 바로 돌려보는 에디터를 직접 만든 기록입니다. 이름은
Cnippet, 걸린 시간은 6시간.핵심은 서버를 쓰지 않았다는 점이에요. 백준처럼 코드를 서버로 보내는 대신, 브라우저 안에서 컴파일하고 실행합니다. WebAssembly로요.
tinycc를 직접 빌드하려다 막힌 이야기, Wasmer로 갈아탄 이야기, XTerm으로 터미널을 붙이며 겪은 리스너 문제까지 그대로 적혀 있습니다.앞 회차에 이어 두 번째로 소개하는 글입니다. 이번엔 성격이 완전히 달라요. 지난번이 "무의식적으로 하던 선택을 꺼내본 글"이었다면, 이번엔 6시간짜리 만들기 기록입니다.
조금 더 들어가 보면
먼저 출발점이 좋습니다. 문제 정의가 아주 구체적이에요.
그리고 다음 문장이 이 글의 성격을 결정합니다. "내가 지금 일하고 있는 영역(프론트엔드)의 레버리지를 살려서."
C를 공부하는데 프론트엔드 기술로 접근합니다. 자기가 이미 잘하는 걸 지렛대로 쓴 거예요.
⚙️ 왜 브라우저에서 컴파일하는가
글이 짚은 대조가 흥미롭습니다.
서버에서 돌리는 방식은 사실 만들기가 훨씬 쉽습니다. 리눅스 컨테이너 하나 띄우고 gcc 부르면 되니까요. 그런데 문제도 명확합니다.
남의 코드를 내 서버에서 실행하는 일이거든요. 무한 루프를 돌리면 CPU를 잡아먹고, 메모리를 할당하면 서버 메모리가 줄고, 파일 시스템을 건드리면 위험합니다. 그래서 채점 사이트들은 샌드박스, 시간 제한, 메모리 제한, 시스템 콜 차단을 겹겹이 쌓아둡니다. 인프라 비용이 그대로 들고요.
브라우저에서 돌리면 이 전부가 사라집니다. 무한 루프를 돌려도 자기 탭이 멈출 뿐이고, 서버 비용은 0이며, 코드가 어디로도 전송되지 않습니다.
🧱 WASM과 WASI — 계산은 아는데 세상을 모른다
글이 정리한 요구사항 세 줄이 문제를 정확히 잡고 있습니다.
2. C 코드조각 string을 컴파일러에 던져 output 파일을 wasm으로 만든다
3. output wasm 파일을 브라우저에서 실행시킨다.
컴파일러 자체를 WASM으로 만들어 브라우저에 올린다는 발상이 핵심입니다. 컴파일러도 결국 프로그램이니까요.
그런데 여기서 막힌 지점이 있었습니다.
stdin, stdout, stderr 같은 걸 WASI로 만드는 것에는.. 내 한계를 보았다."이 문장을 이해하려면 WASM이 무엇을 모르는지를 알아야 합니다.
WebAssembly는 계산만 할 줄 압니다. 숫자를 더하고, 메모리를 읽고 쓰고, 함수를 부르는 것까지예요. 파일을 여는 법도, 화면에 글자를 찍는 법도 모릅니다. 일부러 그렇게 설계됐어요. 아무것도 못 하니까 안전한 겁니다.
그런데 C 프로그램은 printf를 부릅니다. main.c 파일을 읽어야 하고요. 그래서 바깥 세상과 이어주는 규약이 따로 필요한데, 그게 WASI(WebAssembly System Interface)입니다.
브라우저에는 파일 시스템이 없으니, 가짜 파일 시스템을 자바스크립트로 만들어서 WASI 규약에 맞게 연결해줘야 합니다. open, read, write를 전부 흉내내는 일이에요. 저자가 "내 한계를 보았다"고 한 게 이 지점입니다. 작은 일이 아닙니다.
⏱️ 한계를 인정한 자리
그다음 문장이 이 글에서 가장 배울 만한 부분이라고 봤습니다.
목표를 다시 확인하고 방향을 튼 겁니다. 목표가 "WASI를 이해하기"였다면 계속 파고드는 게 맞았을 거예요. 그런데 목표는 "C 에디터를 만들기"였습니다. 그러면 WASI를 대신 해주는 도구를 찾는 게 목표에 더 가깝습니다.
📝 원문에서는 이렇게 해결합니다
찾아낸 도구가 Wasmer입니다. 코드가 놀랄 만큼 짧아요.
// 1. fetch wasm file from wasmer registry
await Wasmer.fromRegistry("clang/clang");
// 2. create a new directory
const project = new Directory();
// 3. write file to directory
await project.writeFile("main.c", codeSnippet);
// 4. compile code snippet
const instance = await clangRef.current.entrypoint?.run({
args: ["/project/main.c", "-o", "/project/main.wasm"],
mount: { "/project": project },
});
// 5. wait for the process to exit
const compileOutput = await instance.wait();
// 6. read wasm file to the wasm runtime.
const wasm = await project.readFile("main.wasm");
// 7. load executable
const program = await Wasmer.fromFile(wasm);
// 8. run the executable
const result = await program.entrypoint?.run();
읽어보면 터미널에서 하던 일과 똑같습니다. 디렉터리 만들고, 파일 쓰고, clang main.c -o main.wasm 하고, 실행. 다른 점은 이게 브라우저 탭 안에서 일어난다는 것뿐이에요.
new Directory()와 mount가 아까 말한 가짜 파일 시스템입니다. 저자가 포기했던 그 부분을 라이브러리가 대신 해주고 있어요. "레지스트리에 clang이 있었다"는 발견이 결정적이었습니다.
⌨️ 터미널을 붙이며 얻은 것
XTerm으로 터미널 UI를 붙인 부분에 실전 교훈이 있습니다.
a 한 번 눌렀을 뿐인데 aa 같이 중복해서 키가 입력되는 불상사가 생길 것이다."이벤트 리스너를 정리하지 않으면 생기는 전형적인 증상입니다. 실행할 때마다 onData를 붙이면 리스너가 쌓이고, 키를 한 번 눌러도 여러 번 처리돼요.
이게 리액트에서 useEffect의 정리 함수를 챙기는 이유와 정확히 같은 문제입니다. 그런데 XTerm.onData는 IDisposable을 돌려주기 때문에 직접 dispose()를 불러줘야 합니다.
인코딩 이야기도 정확합니다.
TextEncoder를 사용하였다. wasm 랜드에서는 바이트 시퀀스만 받을 수 있기 때문이다."자바스크립트 문자열과 WASM 메모리 사이에는 문자열이라는 개념이 없습니다. WASM 쪽은 바이트 배열만 압니다. 그래서 TextEncoder로 바이트로 바꿔서 넘기고, 받을 때는 TextDecoder로 되돌려야 해요. 두 세계의 경계에서 늘 나오는 작업입니다.
큐레이터 노트
시간을 정해놓고 만든 기록이라 뽑았습니다.
"6시간 정도 시간을 들여서"라는 한마디가 이 글의 성격을 다 말해줍니다. 완성도를 무한정 올리는 프로젝트가 아니라 정해진 시간 안에 돌아가는 것을 만든 작업이에요.
그래서 포기한 지점이 그대로 남아 있습니다. tinycc를 포기한 이유, clang이 50MB라 아쉬운 점, "아직 파일과 관련된 부분을 수강하지 않은 터라 기본적인 기능만 구현하였다"는 범위 고백까지요. 성공만 적힌 글보다 훨씬 참고가 됩니다.
마지막 문단도 인상적이었어요.
"깊게 알진 못했지만 낯설지 않게 됐다" — 만들어보는 일의 효용을 정확히 표현한 문장입니다. 나중에 이 단어를 다시 만났을 때 시작점이 완전히 달라지거든요.
02
🦥 게으른(lazy) 계산으로 만드는 무한한 시퀀스 - generateSequence
박지훈 님
Kotlin의
generateSequence를 정리한 글입니다. "다음 값을 어떻게 만들지"만 정의하면 끝없이 이어지는 데이터 흐름을 만들 수 있어요.기본 사용법부터 시작해
null을 반환하면 멈춘다는 종료 규약, 그리고 List와 Sequence의 차이를 표로 정리합니다.백미는 실무 사례예요. 중복 없는 키 생성을
while(true) 루프 대신 시퀀스로 표현합니다. 그 외에 링크드 리스트 순회, API 페이지네이션까지 다뤄요."업무 중 우연히 접했습니다"로 시작합니다. 일하다 만난 함수 하나를 끝까지 파본 글이에요.
조금 더 들어가 보면
먼저 lazy가 왜 다른지부터 보겠습니다.
val numbers = generateSequence(1) { it + 1 }
1부터 시작해 계속 1씩 더합니다. 끝이 없어요. 그런데 이 줄을 실행해도 아무 일도 안 일어납니다. 프로그램이 멈추지도 않고요.
println(numbers.take(5).toList()) // [1, 2, 3, 4, 5]
toList()를 부르는 순간에야 값이 만들어집니다. 그것도 딱 5개만.
글이 정확히 짚습니다. "여기서 take(5)가 없었다면 프로그램은 무한히 값을 만들기만하고 멈추지 않았을 겁니다."
🔀 순서가 다르다 — List와 Sequence
글이 "모든 연산이 lazy하게 동작합니다"라고 쓴 부분을 조금 더 풀어보면, 실행 순서 자체가 달라집니다.
원소 세 개에 map과 filter를 걸어본다고 해볼게요.
List라면 — map을 세 번 다 돌려서 새 리스트를 만들고, 그다음 filter를 세 번 돌립니다. 중간 리스트가 하나 생겨요.
map(1) map(2) map(3) → [a,b,c] → filter(a) filter(b) filter(c)
Sequence라면 — 원소 하나가 map과 filter를 통과한 다음, 그다음 원소가 출발합니다.
map(1) filter(a) → map(2) filter(b) → map(3) filter(c)
중간 리스트가 안 만들어집니다. 원소 3개면 별 차이 없지만 10만 개면 이야기가 달라져요. List는 10만 개짜리 배열을 매 단계마다 새로 만들고, Sequence는 하나씩 흘려보냅니다.
그리고 결정적인 차이가 하나 더 있어요. 만약 앞에서 5개만 필요하다면 Sequence는 5개만 계산하고 멈춥니다. List는 이미 10만 개를 다 처리한 뒤입니다.
🛑 null로 멈춘다는 규약
종료 조건이 재미있습니다.
val limited = generateSequence(1) {
if (it < 10) it + 1 else null
}
println(limited.toList())
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
null을 돌려주면 거기서 끝입니다. 별도의 종료 함수가 없어요. "다음 값"과 "끝"을 같은 자리에서 표현하는 설계입니다.
간결한 대신 대가가 하나 있는데, 진짜 null 값을 담은 시퀀스는 만들 수 없다는 점입니다. null이 데이터가 아니라 신호로 예약돼 있으니까요. 실무에서 걸릴 일은 드물지만, 알고 쓰는 것과 모르고 쓰는 것은 다릅니다.
📝 원문에서는 이렇게 씁니다
가장 실용적인 부분이 중복 없는 키 생성입니다. 저자가 실제 업무에서 쓴 패턴이에요.
val uniqueKey = generateSequence { keyGenerator.generate() }
.first { candidate -> !repository.existsByKey(candidate) }
두 줄로 "고유한 키가 나올 때까지 반복 생성"이 끝납니다.
여기서 first { }가 핵심입니다. 조건을 만족하는 첫 값을 찾는 순간 멈춰요. 그러니까 첫 키가 이미 비어 있으면 딱 한 번만 생성하고 끝납니다. while(true) 루프에 break를 넣은 것과 결과가 같은데, break가 안 보입니다.
이게 글이 말한 지점이에요.
while(true) { ... if (조건) break }는 끝나는 조건을 찾으려면 안을 읽어야 합니다. 위 코드는 .first { 조건 } 한 줄에 다 있어요.
🧭 그리고 이 글이 스스로 고쳐놓은 부분
글에는 같은 로직의 첫 번째 버전도 함께 실려 있습니다.
val uniqueKey = generateSequence { keyGenerator.generate() }
.first { candidate ->
try {
repository.findByKey(candidate)
false // 이미 존재하면 다음 키 생성
} catch (_: Exception) {
true // 존재하지 않으면 사용 가능
}
}
그리고 바로 뒤에 "보다 명시적인 메서드를 사용할 수 있다면"이라며 existsByKey 버전을 제시해요.
이 흐름이 좋았습니다. 자기가 쓴 코드의 약한 부분을 알고 대안까지 붙여둔 것이거든요.
왜 약한지 짚어두면, 위 코드는 "없음"을 예외로 판단합니다. findByKey가 못 찾았을 때 예외를 던진다는 전제인데, 이 전제가 언제든 깨질 수 있어요. Spring Data JPA의 findBy...는 보통 null이나 Optional.empty()를 돌려주고 예외를 안 던집니다. 그러면 catch에 절대 안 걸리고 항상 false가 되어 — 무한 루프가 됩니다.
게다가 catch (_: Exception)은 연결 끊김이나 타임아웃까지 "사용 가능"으로 읽습니다. DB가 잠깐 죽었을 뿐인데 중복 키를 저장하게 되죠.
그래서 아래 버전이 훨씬 안전합니다. existsByKey는 있으면 true, 없으면 false를 명확히 돌려주고, 진짜 장애는 예외로 올라와 멈추게 두니까요. "예외를 흐름 제어로 쓰지 않는다"는 원칙이 여기서 실제 차이를 만듭니다.
🔗 링크드 리스트 순회가 예뻤던 이유
여러 활용 사례 중에서 이게 특히 인상적이었어요.
val result = generateSequence(node1) { it.next }
.map { it.value }
.toList()
// [1, 2, 3]
보통 이렇게 씁니다.
var current = node1
while (current != null) {
result.add(current.value)
current = current.next
}
변수 하나를 계속 바꿔가며 도는 코드죠. 위 코드에는 그런 변수가 없습니다. "다음은 it.next"라고 규칙만 말하면 순회가 만들어져요.
그리고 아까 본 null 종료 규약이 여기서 딱 맞아떨어집니다. 마지막 노드의 next가 null이니까 자연스럽게 끝납니다. 종료 조건을 따로 쓸 필요가 없어요.
큐레이터 노트
하나를 깊게 파고 널리 적용해본 글이라 뽑았습니다.
"업무 중 우연히 접했습니다"로 시작해서 문서 읽고 → 직접 써보고 → 실무에 적용하고 → 다른 쓰임새까지 찾아본 흐름이 그대로 보여요. 함수 하나로 이만큼 나갈 수 있다는 게 좋았습니다.
실무 코드를 가져온 것이 가장 큰 강점입니다. 예제로 만든 1, 2, 3... 수열은 어디에나 있어요. 그런데 "중복 없는 키를 어떻게 만들 것인가"는 실제로 부딪히는 문제입니다. 그리고 저자는 왜 그 방식을 택했는지까지 세 가지로 정리해뒀어요. 루프 제거, 의도 표현, 테스트 용이성.
첫 버전과 개선 버전을 나란히 둔 것도 좋았습니다. 완성본만 보여주는 글은 도착점만 알려주지만, 이 글은 경로를 보여줍니다.
마지막 비교표도 깔끔했어요. 계산 시점 / 메모리 / 적합한 상황 / 구현 난이도. "구현 난이도: 조금 더 구조적"이라고 솔직히 적은 것도 신뢰가 갔습니다. 좋은 점만 나열하지 않았거든요.
03
🧵 JavaScript Web Worker
서지영 님
3,000개짜리 배열을 조작하니 화면이 멈추더라는 실무 문제에서 출발합니다. 해결책으로 Web Worker를 쓴 기록이에요.
네 가지를 순서대로 다룹니다. 왜 멈추는가 → Web Worker가 어떻게 푸는가 → 어떻게 동작하는가 →
setTimeout·Promise와 뭐가 다른가.마지막 질문이 이 글의 백미입니다. "JavaScript는 싱글 스레드인데 Web Worker는 어떻게 멀티스레드로 동작하는가?"
리액트 예제 코드가 워커 생성부터 정리까지 통째로 들어 있어서, 그대로 옮겨 쓸 수 있는 형태입니다.
조금 더 들어가 보면
문제 상황이 아주 구체적입니다.
"화면이 멈춘다"는 표현이 정확합니다. 이건 느려지는 게 아니에요. 아예 아무 반응이 없습니다. 버튼을 눌러도, 스크롤을 해도, 글자를 입력해도 반응이 없다가 계산이 끝나면 한꺼번에 밀린 게 처리됩니다.
이유는 글이 짚은 그대로입니다.
브라우저의 메인 스레드는 자바스크립트 실행과 화면 그리기를 번갈아 합니다. 그런데 번갈아 하려면 자바스크립트가 잠깐씩 손을 놔줘야 해요. 긴 반복문은 손을 놓지 않습니다. 끝날 때까지 붙들고 있죠. 그동안 화면은 한 프레임도 못 그립니다.
🚦 동시성과 병렬성 — 이 글이 가장 잘 설명한 부분
이 글에서 가장 값진 대목은 마지막 비교입니다. 헷갈리기 쉬운 지점을 정면으로 다뤄요.
setTimeout이나 Promise 같은 비동기 함수들은 Web Worker 처럼 별도의 스레드를 새로 만들지 않는다. (…) 반면 Web Worker 는 실제 스레드를 생성하는 병렬성 모델이다.""비동기로 하면 안 멈추지 않나?"라고 생각하기 쉬운데, 그렇지 않습니다.
setTimeout으로 무거운 계산을 감싸면 실행이 미뤄질 뿐 실행되는 순간에는 여전히 메인 스레드를 붙듭니다. 화면은 똑같이 멈춰요. 시작 시점만 옮긴 것이지 일하는 장소를 옮긴 게 아니거든요.
Promise도 마찬가지입니다. fetch가 안 멈추는 건 네트워크 대기를 브라우저가 대신 해주기 때문이지, 자바스크립트가 병렬로 도는 게 아니에요. .then() 안의 콜백은 메인 스레드에서 실행됩니다. 거기에 무거운 계산을 넣으면 그대로 멈춥니다.
정리하면 이렇습니다.
| 새 스레드 | 무거운 계산에 | 쓰는 곳 | |
|---|---|---|---|
setTimeout |
안 만듦 | 화면 멈춤 | 지연 실행 |
Promise/fetch |
안 만듦 | 화면 멈춤 | 기다리는 일 |
| Web Worker | 만듦 | 화면 안 멈춤 | 계산하는 일 |
기다리는 일과 계산하는 일의 구분이 요점입니다. 기다리는 일(네트워크, 타이머)은 비동기로 충분해요. 계산하는 일은 스레드를 나눠야 합니다. 글이 든 "3,000개 배열 조작"은 명백히 후자입니다.
🚪 DOM에 접근할 수 없는 이유
Worker의 대표적 제약도 잘 짚었습니다.
불편해 보이지만 이건 의도된 설계입니다.
두 스레드가 같은 DOM을 동시에 고칠 수 있다면 어떻게 될까요. 한쪽이 노드를 지우는 사이 다른 쪽이 그 노드의 자식을 추가하는 상황이 생깁니다. 그러면 락(lock)이 필요해지고, DOM API 전체가 훨씬 복잡하고 느려집니다.
그래서 아예 막았습니다. Worker는 DOM을 모르고, 메시지만 주고받아요. 대신 fetch나 XMLHttpRequest는 쓸 수 있습니다. 글이 짚은 그대로입니다.
📝 원문에서는 이렇게 씁니다
메시지 구조가 예제에 잘 드러납니다.
// src/worker.ts
self.onmessage = (event: MessageEvent) => {
const message = event.data;
if (message === 'start_calculation') {
let sum = 0;
for (let i = 0; i < 10_000_000_000; i++) {
sum += 1;
}
self.postMessage(sum); // 결과 전송
}
};
100억 번 도는 반복문이 들어 있어요. 메인 스레드였다면 화면이 한참 멈췄을 코드입니다. 여기서는 옆 스레드가 돌립니다.
받는 쪽입니다.
useEffect(() => {
workerRef.current = new Worker();
workerRef.current.onmessage = (event: MessageEvent<number>) => {
setResult(`Calculation Complete: ${event.data}`);
};
workerRef.current.onerror = (error: ErrorEvent) => {
setResult('Error during calculation!');
};
// 컴포넌트 언마운트 시 워커 종료 (메모리 누수 방지)
return () => {
if (workerRef.current) {
workerRef.current.terminate();
workerRef.current = null;
}
};
}, []);
정리 함수에서 terminate()를 부르는 부분이 특히 중요합니다. Worker는 컴포넌트가 사라져도 스스로 죽지 않아요. 페이지를 옮겨 다닐 때마다 새 Worker가 생기고 옛 Worker가 계속 돌면, 스레드가 쌓입니다. 앞서 오세명 님 글의 리스너 정리와 정확히 같은 종류의 문제예요.
예제 화면에 입력창을 하나 넣어둔 것도 좋았습니다.
계산이 도는 동안 타이핑이 되는지 직접 확인시켜주는 구성이에요. 말로 "안 멈춥니다" 하는 것보다 훨씬 설득력이 있습니다.
📮 데이터가 복사된다는 것
글이 짚은 이 문장이 실무에서 중요합니다.
메인 스레드에서 postMessage로 배열을 보내면 Worker에는 사본이 갑니다. Worker가 그 사본을 고쳐도 원본은 그대로예요. DOM 접근을 막은 것과 같은 이유입니다. 공유하지 않으니 꼬일 일이 없습니다.
다만 복사에는 비용이 있어요. 3,000개 정도면 무시할 만하지만, 아주 큰 배열을 왔다 갔다 시키면 복사 시간이 계산 시간보다 커질 수 있습니다. 그때는 Transferable(소유권을 넘기고 원본은 못 쓰게 되는 방식)이나 SharedArrayBuffer 같은 걸 찾아보게 되는데, 일단은 이 글의 방식으로 충분합니다. 필요해졌을 때 다음 단계가 있다는 것만 알아두면 좋아요.
큐레이터 노트
증상에서 시작해 원리까지 간 글이라 뽑았습니다.
시작이 "Web Worker를 공부했다"가 아니라 "화면이 멈춰버리는 현상이 생기게 되었다"입니다. 실제로 겪은 문제예요. 그리고 해결한 다음에 왜 그랬는지를 파고들었습니다.
특히 "JavaScript는 싱글 스레드 언어인데 Web Worker는 어떻게 멀티스레드로 동작하는가?"라는 질문을 스스로 던진 게 좋았어요. 문제가 이미 해결된 뒤에 나오는 질문이거든요. 여기서 멈춰도 됐는데 멈추지 않았습니다.
그리고 답이 정확합니다. "JavaScript 언어 자체는 싱글 스레드가 맞다"로 인정하고, "실행되는 환경(웹 브라우저, Node.js)은 여러 기능과 스레드를 지원"한다고 구분해요. 언어와 런타임을 나눠서 보는 것 — 자바스크립트를 오래 쓴 사람도 헷갈리는 구분입니다.
글 구성도 친절합니다. 앞머리에 "이 글에서는 (…) 기술해보려고 한다"로 다룰 것을 예고하고, 그 순서대로 갑니다. 읽는 사람이 길을 잃지 않아요.
💡 세 편을 겹쳐 읽으며
세 글의 언어가 다 다릅니다. C와 WebAssembly, Kotlin, TypeScript.
그런데 셋 다 같은 종류의 결정을 다루고 있어요. "이 계산을 지금, 여기서 해야 하는가?"
오세명 님은 장소를 옮겼습니다. 원래 서버에서 하던 컴파일을 브라우저로요. 그랬더니 서버 비용도 샌드박스도 필요 없어졌습니다.
박지훈 님은 시점을 옮겼습니다. 미리 다 계산하던 걸 필요할 때로요. 그랬더니 무한한 데이터를 다룰 수 있게 됐습니다.
서지영 님은 스레드를 옮겼습니다. 메인에서 돌던 계산을 옆으로요. 그랬더니 화면이 안 멈춥니다.
계산의 내용은 하나도 안 바뀌었습니다. C 코드는 똑같이 컴파일되고, 수열은 똑같이 만들어지고, 3,000개 배열은 똑같이 순회됩니다. 바뀐 건 언제, 어디서뿐이에요.
그리고 그 하나를 바꾸니까 원래 못 하던 게 됩니다. 성능 최적화라기보다 가능성의 문제에 가까워요.
한 가지 더 겹치는 게 있습니다. 세 글 다 뒷정리를 이야기합니다. dispose(), null 종료, terminate(). 계산을 다른 자리로 옮기면 그 자리를 치우는 일이 새로 생기거든요. 옮기는 것만 배우고 치우는 걸 안 배우면 누수가 남습니다.
이번 회차에서 가져갈 것
1. WASM은 계산만 안다.
파일도 입출력도 모릅니다. 바깥 세상과 이어주는 규약이 WASI예요. 브라우저에서 뭔가 실행시키려면 가짜 파일 시스템을 만들어줘야 하는데, Wasmer 같은 라이브러리가 그걸 대신 해줍니다.
2. 시간을 정해두면 결정이 빨라진다.
"6시간"이라는 상한이 있었기에 tinycc를 포기하고 라이브러리로 넘어갈 수 있었습니다. 목표가 '이해하기'인지 '만들기'인지를 확인하면 어디까지 파야 할지가 정해져요.
3. Sequence는 순서가 다르다.
List는 map을 전부 돌리고 filter를 전부 돌립니다. Sequence는 원소 하나가 끝까지 통과한 뒤 다음 원소가 출발해요. 중간 리스트가 안 생기고, 앞의 몇 개만 필요하면 그만큼만 계산합니다.
4. while(true) + break는 first { }로 바꿔볼 수 있다.
끝나는 조건이 루프 안이 아니라 한 줄에 드러납니다. "조건을 만족할 때까지 반복"이 그대로 코드가 돼요.
5. "없음"을 예외로 판단하지 말 것.
try { findByKey() } catch { 사용가능 } 형태는 조회 메서드가 예외를 안 던지면 무한 루프가 되고, 연결 장애까지 "사용 가능"으로 읽습니다. existsByKey처럼 참/거짓을 직접 주는 메서드를 쓰세요.
6. 비동기와 병렬은 다르다.
setTimeout과 Promise는 새 스레드를 안 만듭니다. 무거운 계산을 넣으면 화면은 똑같이 멈춰요. 기다리는 일은 비동기로, 계산하는 일은 Web Worker로 나눠 생각하세요.
7. 옮겼으면 치워야 한다.
XTerm.onData는 dispose(), Web Worker는 terminate(). 리액트라면 useEffect 정리 함수에서요. 안 치우면 리스너와 스레드가 쌓입니다.
좋은 글 남겨주신 오세명 님, 박지훈 님, 서지영 님께 감사드립니다! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.