
이 포스트의 제목은 유튜브 영상 <너만의 월남쌈을 싸 - 아이네 INE>에서 따왔다.
4월 초부터 @ilokesto 네임스페이스에 속한 거의 모든 라이브러리를 리뉴얼 하고 있다. 이미 deprecated 처리는 끝났고, 새로운 기준 위에서 라이브러리의 구조를 재정의하는 단계를 거치고 있다. 지난 2년 동안 소중히 키워온 라이브러리들이다. 아쉽지 않다면 거짓말이고, 마음 한 편이 계속 불편하지만, 지금의 선택이 더 나은 방향으로 나아가기 위한 필연적인 과정이라고 생각하고 있다.
라이브러리 자체에는 문제가 없었고, 원한다면 그 모습 그대로 계속 쓸 수도 있었다. 하지만 리뉴얼을 하지 않고서는 결코 해결할 수 없는 문제가 두 가지 있었다. 하나는 각각의 라이브러리가 리액트에 너무 강하게 종속되어있다는 점이었다. 전역 상태 관리 라이브러리도, form 상태 관리 라이브러리도, 모달 상태 관리 라이브러리조차 리액트 외의 환경에서는 전혀 쓸 수가 없다.
두 번째 문제가 더 심각한데, 전역 상태 관리 라이브러리의 이름은 caro-kann 이고, 나머지 둘도 각각 sicilian과 grunfeld이다. 내가 체스에 미친놈이라는 사실은 잘 알려주지만, 각각의 라이브러리가 뭐하는 놈들인지 딱 이름만 보고서는 도무지 알 방법이 없다. 그래서 꽤 오랫동안 '바꿔야지'하고 생각만 하고 있었다가 이번 기회에 싹 갈아엎게 된 것이다.
리뉴얼을 진행하다 문득 그런 생각이 들었다. 왜 나는 아무도 쓰지 않는 (물론 주변 분들 몇몇이 써주기는 하지만) 라이브러리를 만들고 관리하는데 이렇게 많은 시간을 쓰고 있는 걸까? 대강의 이유는 있지만 명쾌하게 설명하기란 여간 쉽지 않다.
그래서 명쾌한 설명 대신 기나긴 잡생각의 형식으로 내 생각을 풀어보고자 한다.
내 손에 딱 맞는 도구가 있다는 것
나는 부트캠프 출신이고 처음에는 이런저런 라이브러리를 가져다 사용했다. 전역 상태 관리 라이브러리만 해도 Zustand Jotai Redux Toolkit recoil 등을 써봤고, form 관리가 필요할 때는 React Hook Form과 formik을 썼다. 뭐 하나 딱 이거다 싶은 건 없었지만, 라이브러리 없이 각각의 기능을 바닥부터 구현하는 것도 멀쩡한 생각은 아닌지라 꾹 참을 수 밖에 없었다.
그것도 반 년이 채 못가긴 했지만 말이다.
여러 라이브러리를 배회한 끝에 Zustand에 정착했지만, 여기도 문제는 있었다. 제일 귀찮은 문제는 매번 전역 상태를 만들 때마다 타입을 지정해줘야 한다는 것이었다. 타입스크립트의 끝내주는 타입 추론의 힘을 빌리면 쉽게 해결할 수 있을 거 같은데, 왜 이걸 해결하지 않고 꼬박꼬박 제네릭 타입으로 전역 상태의 형태를 제공해야 하는지 이해할 수 없었다.
import { create } from 'zustand'
type Store = {
count: number
inc: () => void
}
const useStore = create<Store>()((set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const { count, inc } = useStore()
return (
<div>
<span>{count}</span>
<button onClick={inc}>one up</button>
</div>
)
}지금 와서 돌아보면 정말 오만한 생각인데, 나는 이 문제를 해결할 수 있을 거라 생각했다. 그래서 Zustand를 포크해 내부 구현을 직접 뜯어고치기 시작했다. 사용자가 별다른 선언 없이도 자연스럽게 타입 추론이 되게 만들자. 당연히 대차게 실패했지만 이 시도가 완전히 헛된 건 아니었다. 타입을 맞추기 위해 내부 구조를 계속 파고들다 보니, 전역 상태 관리 라이브러리가 어떤 구성 요소로 이루어져 있고, 각 요소가 어떤 책임을 가지는지 자연스럽게 체득하게 됐다.
'내 입맛에 맞는 라이브러리를 하나 만드는 게 낫겠는데?' 내 첫 라이브러리는 그렇게 탄생했다. 당시 주력 오프닝으로 카로칸 디팬스를 써먹고 있었던지라, 나는 별 다른 생각 없이 라이브러리 이름을 caro-kann으로 지었다.
const useBoard = playTartakower({
name: "Caro-Kann",
age: 32,
canStand: true
});
function Component() {
const [board, setBoard] = useBoard();
// ...중략
}caro-kann은 진정한 의미에서 나를 위해 존재하는 라이브러리였다. 내가 느끼는 불편함을 스스로 해결한 첫 번째 사례였고, 그 자체로 기념비적인 존재였다. 그리고 무엇보다 내 손에 딱 맞는 도구였다. 너무 당연한 소리를 하는 걸지도 모르지만, 만약 어떤 도구가 손에 완벽히 들어맞는다면, 그 도구만 보고도 손의 형태를 짐작해볼 수 있지 않을까?
caro-kann은 내 손에 정확히 맞는 도구였고, 그 안에는 자연스럽게 나의 선택과 습관, 그리고 문제를 바라보는 방식이 스며들어 있었다. 그래서 이 라이브러리를 설계해나가는 과정은 단순히 어떤 기능을 어떻게 구현할 지 고민하는 것 뿐만 아니라, 내가 어떤 성향을 가진 개발자인지를 드러내고 다시 이해하게 되는 경험이기도 했다.
나를 성장시키는 도구
나는 부트캠프를 졸업하고서 여러 곳에 이력서를 냈고, 꽤 많은 곳에서 개발 과제를 받았다. 지금도 그런지는 모르겠지만, 당시에는 프론트엔드 과제라면 규모에 상관 없이 백이면 백 전역 상태 관리 라이브러리를 사용하라는 요구가 포함되어있었다. 보통은 특정 라이브러리를 지정하지 않아서, 나는 모든 과제를 caro-kann과 함께했다. 직접 만든 전역 상태 관리 라이브러리라는 점에서 좋은 점수를 얻을 수 있었고, 늘 면접때 관련 질문이 뒤따랐다.
가끔은 caro-kann으로 해결할 수 없는 요구사항도 있었다. 가령 이름이 기억나지 않는 어떤 회사의 과제에는 이런 요구사항이 있었다.
전역 상태 관리 라이브러리를 사용하여 상품 카트 기능을 구현하세요. 상품을 담는 페이지와 카트를 확인하는 페이지는 분리되어있어야 하며, 화면을 새로고침해도 카트에 담은 상품은 유지되어야 합니다.
처음에는 단순히 store의 값을 localStorage에 저장하고, 초기화 시 다시 불러오면 되지 않을까 생각했다. 하지만 실제로 구현을 시작해보니 문제는 생각보다 단순하지 않았다. 상태 변경 타이밍마다 직렬화하여 저장해야 했고, 초기 hydration 과정에서의 동기화 문제, 그리고 특정 상태만 선택적으로 persist해야 하는 요구까지 고려해야 했다. 결국 이 요구사항은 단순한 전역 상태 관리가 아니라, 상태의 생명주기를 외부 스토리지와 동기화하는 문제였고, caro-kann으로는 해결할 수 없는 문제였다.
내 결론은 단순했다. 그래서 나는 과제를 하다 말고 caro-kann을 뜯어고쳤다. 전역 상태를 선언하면서 동시에 두 번째 인자로 옵션 객체를 넘겨주면, 내부적으로 상태 변경 로직을 몽키패치하여 로컬 스토리지나 쿠키에다가 값을 자동으로 저장하도록 만든 것이다.
단순히 set 시점에 저장하는 수준이 아니라, 초기화 단계에서 해당 키를 기준으로 값을 읽어와 store를 hydrate하는 과정까지 포함시켰다. 이 과정에서 자연스럽게 직렬화와 역직렬화 로직을 분리하게 되었고, 어떤 상태를 persist할지 선택할 수 있도록 필터링 옵션도 추가했다. 결과적으로 caro-kann은 단순한 전역 상태 관리 도구에서 한 단계 확장되어, 상태의 생명주기를 외부 저장소와 동기화할 수 있는 구조를 갖추게 되었다.
// 초기 caro-kann의 persist 선언
type Theme = { color: "light" | "dark", fontSize: number };
const { useBoard: useThemeBoard } = playTartakower(
{ color: "light", fontSize: 16 },
{
local: "theme",
migrate: {
version: 1,
strategy: (prevState, prevVersion) => {
return { color: prevState, fontSize: 16 };
},
},
}
);이런 경우도 있었다. 과제를 구현하고 있는데 상태의 변화가 내 예상과 크게 다르게 흘러가는 순간이 있었다. 매번 그러는 것은 아니었지만 가끔이라도 예상과 다르게 동작한다는 건 충분히 거슬리는 일이었다. 방법이 없어서 node_module 내의 패키지 코드에 직접 console.log를 박으면서 디버깅을 했는데, 한두번은 그럴 수 있어도 매번 디버깅을 해야 할 때마다 라이브러리 내부를 직접 뒤져야 한다는 건 도구를 사용하는 입장에서 꽤 피로한 일이었다.
그래서 상태가 변할 때마다 어디서 어떤 상태가 어떤 값에서 어떻게 변했는지를 콘솔에 기록해주는 기능을 만들기로 했다. 문제는 playTartakower 함수에 직접 persist 로직을 박아두었던 까닭에 새로운 기능을 추가하기가 여간 까다로운 게 아니었다. 만약 persist 없이 devtools만 필요한 경우에는 어떻게 해야하지? persist와 devtools 둘 다 필요하다면 그 때는 또 어떻게 해야 할까? 그런 고민 끝에 미들웨어라는 답에 도달했다.
// 미들웨어는 caro-kann@3.0.0에서 처음 등장했다
// 잘 보면 playTartakower 함수의 이름이 create로 바뀌었다
type Theme = { color: "light" | "dark", fontSize: number };
const { useBoard: useThemeBoard } = create(
persist(
devtools(
{ color: "light", fontSize: 16 },
"themeStore"
),
{
local: "theme",
migrate: {
version: 1,
strategy: (prevState, prevVersion) => {
return { color: prevState, fontSize: 16 };
},
},
}
)
);caro-kann에 맞는 미들웨어를 구현하기 위해서는 먼저 상태 변경이 어떤 지점을 통과하는지부터 다시 정의해야 했다. 단순히 store 바깥에서 기능을 덧붙이는 방식으로는 persist처럼 상태를 저장하는 일도, devtools처럼 상태 변화의 원인과 결과를 가로채 기록하는 일도 깔끔하게 처리할 수 없었기 때문이다. 그래서 나는 상태 생성, 변경, 구독의 흐름 사이에 개입할 수 있는 얇은 계층을 만들고, 각 기능이 store 자체를 오염시키지 않으면서도 동일한 방식으로 확장될 수 있도록 구조를 다시 설계했다. 그 결과 persist와 devtools는 더 이상 예외적인 부가기능이 아니라, caro-kann의 상태 흐름 위에 일관된 방식으로 연결되는 확장 지점이 되었다.
오래된 격언 중에 “바퀴를 다시 발명하지 말라”는 말이 있다. 이미 잘 만들어진 것을 굳이 다시 만들 필요는 없다는 뜻인데, 아이러니하게도 나는 그 바퀴를 다시 만드는 과정 속에서 오히려 많은 것을 배웠다. 미들웨어 구조를 직접 구현하면서 단순히 기능을 따라 만드는 것이 아니라, 왜 이런 구조가 필요한지, 어떤 지점에서 확장이 막히는지, 그리고 그걸 어떻게 구현해야 할지 뇌가 빠지게 고민했다. 결국 중요한 건 바퀴를 다시 만들었느냐가 아니라, 그 과정을 통해 “왜 바퀴가 그 모양이어야 하는지”를 이해했느냐였다.
그리고 이것들은 라이브러리를 만들지 않았더라면 절대 마주하지 않았을 고민들이었다. 돌이켜보면 내가 만든 것은 단순한 라이브러리가 아니라, 그런 사고 방식을 체득하게 만든 일련의 과정에 가까웠다. 어떻게보면 caro-kann이 나를 키웠다고도 할 수 있겠다.
나와 함께 성장하는 도구
초기 sicilian은 정말 엉망이었다. sicilian이 뭐하는 라이브러리인지 기억나지 않는 사람이 많을텐데 (사실 이것 때문에 리뉴얼을 하고 있는 셈인데) form 관리 라이브러리다. sicilian은 꽤 초기 버전부터 인풋 값을 검정하기 위한 옵션을 받았는데, 이 validation 로직이 정말 끔찍했다. 새로운 조건을 추가하고 싶어도 (내가 짠) 망할 스파게티 코드 때문에 엄두가 나질 않았다.
그러다가 우연히 <디자인 패턴의 아름다움>이라는 책을 접하고, 책임 연쇄 패턴을 알게되었다. 그 즉시 validation 로직을 싹 지우고 해당 부분을 책임 연쇄 패턴을 사용해 처리했다. 각 검증은 더 이상 서로 얽혀 있지 않고, 하나의 체인 위에서 자신의 책임만 수행한 뒤 다음 단계로 넘기는 구조가 되었다. 그 결과 validation 흐름은 훨씬 단순해졌고, 어떤 검증이 어떤 순서로 실행되는지도 명확하게 드러나기 시작했다.
새로운 기능을 추가하는 것도, 이미 있는 기능을 더 좋은 구조로 개선하는 것도 성장이다. caro-kann도 sicilian도 그리고 grunfeld도 처음에는 상상할 수 있는 모든 방식으로 엉망진창이었다. 이딴 꼬라지의 코드가 굴러가다니 예수 부처 알라 중 하나는 실존하나보다 생각이 들 정도로 말이다. 하지만 그 모든 순간을 겪고 도착한 가장 최신 버전의 코드를 보면 꽤나 그럴듯하다. 나중에 보면 또 형편없다 생각할지도 모르지만 당장은 만족스럽다.
라이브러리의 커밋 히스토리를 따라가다 보면 기능이 추가된 흔적보다, 생각이 바뀐 흔적이 더 선명하게 보인다. 어떤 시점에는 무작정 구현에 집중했고, 또 어떤 시점에는 구조를 먼저 고민하기 시작했고, 그 변화가 코드에 그대로 남아 있다. 결국 이 저장소는 단순한 결과물이 아니라, 내가 어떤 식으로 문제를 이해하고 풀어왔는지를 기록한 로그에 가깝다.
딱 내가 성장한 만큼 라이브러리도 성장했다.
그래서 결론이 뭔데?
나는 만나는 사람들에게 가끔 라이브러리 만들기를 권한다. 내 손에 딱 맞는 라이브러리가 있다는 건 그 자체로도 즐거운 일이고, 그걸 만드는 과정 속에서 나 자신이 어떤 개발자인지 깨닫게 된다는 점에서는 일종의 정신수양이기도 하다. 라이브러리를 만들면서 하게 되는 고민이 나를 정말 빠르게 성장시켜줬고, 내가 성장한 만큼 라이브러리도 성장했다는 사실을 깨닫는 순간은 꽤 묘한 쾌감을 준다.
결국 라이브러리를 만든다는 건 단순히 재사용 가능한 코드를 만드는 일이 아니다. 문제를 어떻게 정의하는지, 어떤 기준으로 구조를 선택하는지, 그리고 복잡함을 어디까지 허용할 것인지에 대한 자신의 판단을 코드로 남기는 일에 가깝다. 그래서 나는 좋은 라이브러리를 만들었다고 말하기보다, 그 과정을 통해 조금 더 나은 개발자가 되었다고 말하는 편이 더 정확하다고 생각한다.
우리는 고기쌈 하나를 싸도 사람마다 다른 쌈을 싸는 민족이다. 당신도 당신만의 고기쌈을 싸고, 당신만의 월남쌈을 싸고, 당신만의 라이브러리를 만들어봤으면 좋겠다. 내가 느낀 그 모든 기쁨과 고통과 고민을 한 번 느껴봤으면 좋겠다.
그건 꽤 재미있는 일이니까.
더 읽어보기
2025.04.25
더 좁은 타입의 유효성에 대하여
사람이 무언가를 집중해서 바라보다 보면, 어느샌가 주변부가 흐려지고 가끔은 집중하고 있던 그 대상조차 보이지 않게 된다. 처음엔 분명하게 인식되던 경계가 서서히 사라지고, 오히려 애써 무시했던 주변이 본질을 가릴 때도 있다. 잘 보려 애쓰는 행위가, 역설적으로 시야를 좁히는 순간이다.…
2024.04.01
문제를 해결하기 위한 수단으로서의 프로그래밍
최근 몇 주 동안 프로그래머스에서 알고리즘 문제를 풀고있다. 하루에 몇 문제씩 풀다보니 어느새 레벨1 문제가 네다섯 개 정도밖에 남지 않았다. 이것까지 다 풀면 레벨2로 넘어가야지 생각하고 있다. 코드잇 스프린트 기간 동안 알고리즘에 대한 생각은 전혀 하지 못했고, 때문에 처음에 문제를…
2024.03.04
코드잇 스프린트 1기를 수료하며
아주 오래된 질문 나는 한국에서 남자 간호사 만큼이나 보기 드문 남자 영양사이다. 대학을 식품영양학 전공으로 졸업했고 직장 생활도 영양사 자격으로 시작했다. 그래서일까 최근 자기소개를 하다보면 "어떻게 하다가 코딩에 관심을 가지게 되었어요?" 하는 질문을 자주 받고 있다. 확실히 영양사…
2024.01.09
헤밍웨이의 자살
대부분의 부모님이 그러하듯 우리 부모님도 내가 어렸을 때 여러 위인전을 사서 책꽂이에 구비해두셨다. 정말 어렸을 때는 만화로 된 위인전이었고, 조금 컸을 때는 줄글에 삽화가 자주 나오는 위인전이었다. 양쪽 모두에 헤밍웨이가 있었다. 그리고 어린이를 위한 위인전치고는 이상하리만치 그의 말…
2026.04.17
Code Server
처음 코드 서버를 만들었던 건 아마도 3년쯤 전의 일이다. 맥미니만 있던 탓에 밖에서 개발하는 게 쉽지 않았고, 아이패드로 언제 어디서든 개발을 하고 싶었던 끝에 찾아낸 해결책이었다. 다행히 집에는 Synology NAS가 있었고, Docker를 통해 어렵지 않게 코드 서버를 만들 수…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다. Trie는 무…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
댓글
댓글을 불러오는 중...