Wealthsimple: 인수 1년 후
TMTWealthsimple이 Fey를 인수한 지 벌써 1년이 지났다는 게 믿기지 않습니다. 인수를 겪는 창업자로서 지난 1년이 어땠는지, 그 과정에서 무엇을 배웠는지, 저희 세 사람이 지금까지 무엇을 해냈는지 이야기해 보려 합니다. 이 글은 성능만 다루지는 않습니다. 물론 성능 이야기도 잔뜩 있습니다.
Fey를 만든 저희 세 사람은 10년 넘게 함께 일하면서 저희 일에 끊임없이 몰두해 왔습니다. 서로를 만난 게 얼마나 운이 좋았는지 자주 이야기합니다. 하지만 저는 늘 그건 운이 아니라 필연이었다고 말해 왔습니다.
Tom이나 Thiago에게 물어보면 제가 입에 달고 사는 말을 알려 줄 겁니다. 진짜는 진짜를 알아본다는 말입니다(Real recognize real). 딱 알아듣는 사람을 만났을 때 드는 그 느낌입니다. 몇 년을 몰두하고 고생해야 겨우 드러나는 디테일을 알아채는 사람, 대부분은 평생 눈치채지 못하는 것들을 깊이 아끼는 사람 말입니다.
그런 사람은 정말 드뭅니다. 만나는 순간 바로 압니다. 진짜는 진짜를 알아봅니다. 그게 저희 우정과 창업 성공의 비결이었습니다. 저희는 그 느낌을 함께 갖고 있습니다.
감을 잡기까지
처음 며칠은 문화 충격이었다고 말해도 됩니다. 세 명짜리 팀에서 천 명이 넘는 조직으로, 그것도 나라에서 규제가 가장 심한 산업 중 하나로 옮겨 오는 건 만만한 일이 아닙니다. 온보딩은 2주 일정으로 잡혀 있었지만 저희는 하루 만에 끝내고 곧바로 배포하고 디자인하기 시작했습니다. Tom은 미리 자기 장비를 세팅해 두고 첫 출근일 전에 온보딩 대부분을 이미 끝내 놨던 걸로 기억합니다.
저희가 가장 신경 쓴 곳은 GitHub과 Figma였습니다. Wealthsimple의 클라이언트 코드는 전부 하나의 모노레포에 들어 있습니다. 모바일 앱, 웹 앱, 내부 도구가 모두 여기 있습니다. 회사에서 가장 큰 저장소이고, 가장 많은 엔지니어가 이 안에서 일합니다. Figma는 완전히 다른 차원이었고, 파일이 수백 개는 되는 것 같았습니다.
Fey에서는 코드 한 줄 한 줄을 제가 직접 썼거나 어디에 있는지 짚어 낼 수 있었습니다. 버그가 나면 어떤 파일을 열어 어디를 봐야 하는지 정확히 알았습니다. 여기서는 감을 잡는 데 시간이 조금 걸렸습니다.
다행히 1년 전에도 AI는 이미 대단했기 때문에 코드베이스를 익히는 데 AI에 많이 의지했습니다. 그렇다고 처음 몇 주를 코드베이스 공부에만 쓴 건 아닙니다. 새 기능과 개선 사항을 담은 풀 리퀘스트도 함께 올렸습니다. 10년 동안 직접 스타트업을 갈아 만들며 몸에 박힌 게 있습니다. 무언가를 내보내고 가치를 더해야 한다는 감각입니다.
지금 돌아보면 1년 전에 일하던 방식은 오늘과 꽤 많이 다릅니다. 이 이야기는 뒤에서 더 하겠습니다.
먼저 저희가 합류했을 때 Wealthsimple 웹 앱이 어떤 상태였는지 되짚어 보겠습니다. 대략 이런 모습이었습니다.
Shell
angular 1.8.2 (2022년 1월 지원 종료. 라우팅, 인증, 프레임을 담당)
@uirouter/angularjs (앱의 실제 라우터)
react2angular (모든 React 화면을 셸에 연결)
ng-redux · angular-translate · angular-moment · angular-local-storage
Web
react 19.0.0 + react-dom (셸 안에 마운트)
react-router-dom v5 (260개 파일이 직접 import)
graphql 16 + apollo client 3 (데이터 계층)
redux · thunk · observable · ng-redux (상태 관리. 세 세대가 겹쳐 있음)
connected-react-router (상태와 내비게이션을 붙여 놓음)
patchwork (디자인 시스템)
styled-components + .scss 파일 151개
Toolchain
yarn 1.22 · nx 20 · typescript 5.8 · rspack
eslint 8 + htmlhint
jest 29 · storybook + chromatic (스토리 1,239개)
playwright 1.36 (e2e 스펙 61개. 전부 라이브 스테이징 대상)
Codebase
apps/web/retail/src (파일 659개. 그중 428개가 순수 .js)알고 보니 웹 앱은 Angular 셸 안에서 React가 돌아가는 구조였습니다. 인수 전에 저는 이들의 웹 앱을 리버스 엔지니어링해서 Angular를 쓴다는 건 알아냈지만, 코드베이스를 직접 보지 않고는 얼마나 깊이 들어가 있는지 알 수 없었습니다. 답은 이랬습니다. Angular가 여전히 라우팅과 인증, 앱 전체의 최상위 프레임을 담당하고 있었고, 모든 React 화면은 react2angular라는 브리지를 통해 그 안에서 렌더링됐습니다. 이음새에서 그게 느껴졌습니다. 예를 들어 로그인은 Angular 셸 안에서 이뤄지고, 성공하면 페이지를 통째로 새로 고쳐서 React 홈페이지로 제어권을 넘겼습니다.
상태 계층도 비슷한 이야기였습니다. 예전 화면들은 redux-thunk와 redux-observable을 붙인 Redux가 데이터를 먹여 주고 있었고, connected-react-router가 상태와 내비게이션을 붙여 놨고, 새로 만든 화면들은 더 새로운 패턴을 쓰고 있었습니다.
컴포넌트 작업 흐름은 Storybook에 Chromatic 시각 회귀 테스트를 붙여 쓰고 있었고, 디자인 언어는 Patchwork라는 내부 디자인 시스템 위에 올라가 있었습니다.
파고들어 보니 성능을 끌어올릴 여지도 상당했습니다. 초기 로딩이 느렸습니다. 회사 로고를 런타임에 쿼리 하나당 하나씩 받아 왔고, 폰트는 10년짜리 캐시 헤더를 달고 나가서 사용자가 몇 년 전 폰트 바이너리에 갇힐 수도 있었습니다.
세션 부트스트랩은 기능 플래그가 초기화되기 전에 GraphQL 왕복 요청을 세 번 순차로 보냈고, 그것만으로 콜드 로딩에 5초에서 8초가 더 붙었습니다. 화면을 이동할 때는 라우트 서브트리 전체를 다시 만들었고, 서스펜스 경계 설정이 잘못돼 있어서 전환마다 자바스크립트를 받아 오는 동안 빈 화면이 보일 수 있었습니다.
프리페치는 아예 없었고, Apollo 요청 대부분이 network-only를 쓰고 있어서 데이터가 이미 메모리에 있는데도 스피너를 봐야 했습니다. 하나하나 떼어 놓고 보면 그렇게 심각한 문제는 아니었지만, 다 합쳐 놓으면 확실히 체감됐습니다.
Wealthsimple을 투자하고 은행 업무를 보기에 가장 좋은 곳으로 바꾸려면 기반부터 제대로 잡아야 한다는 걸 알았습니다. 지금 상태로는 세계 최고 수준의 경험을 만들 수 없었습니다. 그래서 화려할 것 없는 기초 작업을 제가 맡기로 했습니다. 그 위에서 무언가를 만드는 사람들이 빛날 수 있도록 말입니다.
이제 재미있는 부분입니다!
제 SSH 키가 등록된 지 이틀 만에 첫 PR이 머지됐습니다. Fey의 종목 로고를 Wealthsimple로 가져오는 작업이었습니다. Fey에서 저희는 수천 개 종목의 로고를 모으고 다듬는 데 말이 안 되는 시간을 썼습니다. 선명한 로고가 붙은 종목 줄은 완성된 느낌을 주는데, 흐릿한 로고가 붙으면, 더 나쁘게는 글자 두 개가 박힌 회색 동그라미가 붙으면 뭔가 고장 난 것처럼 보이기 때문입니다. 사흘 뒤에는 ETF에 맞는 로고를 골라 주는 로직을 올렸습니다. 합류 1주일 차에는 Tom이 메인 앱에 첫 PR을 넣었습니다. 거래 화면에서 전역 키보드 단축키 시스템을 시험해 보는 작업이었습니다. Tom은 거기서 멈추지 않았습니다. 한 달 안에 단축키에는 제대로 된 소개 흐름이 붙었고, Tom이 만든 미체결 주문용 플로팅 독이 배포됐고, 그 독은 앱 전체를 이동하는 법까지 익히고 있었습니다.
이렇게 시작했다는 게 저는 마음에 듭니다. 저희가 가장 먼저 내보낼 수 있는 것 중에 가장 화려하지 않고 가장 저희다운 걸 하나 꼽으라면 로고일 겁니다. 로고를 로드맵에 올리는 사람은 없습니다. 하지만 모든 표의 모든 줄에서 매일 눈에 들어오고, 이 앱을 만드는 사람들이 정말 신경 쓰는지 아닌지를 조용히 알려 주는 디테일이 바로 로고입니다.
첫 주에는 코드베이스에 대해 가장 중요한 사실도 배웠습니다. 패키지가 천 개를 넘는 Nx 모노레포이고, main에 머지되는 순간 프로덕션에 배포된다는 것입니다. 릴리스 트레인도 없고 대기 큐도 없습니다. 머지하면 바로 라이브입니다. 이 사실을 알고 나면 남은 병목은 자기 자신뿐입니다.
한눈에 보는 미체결 주문 독
전역 단축키. H를 누르면 홈으로 갑니다.
개념 증명
1년 동안 일어난 일 중에서 파급력이 가장 컸던 하나를 꼽아야 한다면 개념 증명입니다. Thiago는 Wealthsimple 웹의 새로운 아트 디렉션을 그려 놨는데, 기존 웹 앱과는 전혀 닮지 않은 첫 디자인 모음이었습니다. 여기서 당연히 나오는 질문은 이 디자인이 실제와 부딪혀도 버티느냐였습니다. 진짜 데이터, 진짜 계좌, 진짜 주문 말입니다. Tom이 나서서 project/web-home이라는 브랜치에 "Web art direction POC"라는 제목의 드래프트 PR을 만들고, 웹이 어떤 모습일 수 있는지를 직접 구현했습니다.
단 5일 만에 빈 패키지가 돌아가는 앱이 됐습니다. 커밋 로그는 이렇게 흘러갔습니다. 11월 24일 첫 커밋, 이어서 폰트 테스트, 종목 페이지 속도 테스트, 주문 흐름 세팅, 주문 확인, 검색 세팅, 검색 키보드 내비게이션, 그리고 11월 29일에 내비게이션 완성입니다. 아카이브될 때는 커밋 100개가 넘고 파일이 약 300개까지 늘어나 있었습니다. 홈 페이지, 종목 페이지, 지갑과 자동 입금이 들어간 계좌 상세, 포트폴리오 표, 검증과 테스트까지 갖춘 완전한 주문 흐름, 옵션 체인, 커맨드 팔레트, 키보드 단축키, 그리고 자체 아이콘 세트가 담겨 있었습니다.
분명히 말해 두자면 이건 Tom의 자식이었습니다. Tom은 정말 몰아붙였습니다. 로그의 첫 열흘은 Tom 혼자 찍은 커밋 66개입니다. 저는 열흘 남짓 뒤에 제 커밋 38개를 들고 합류했습니다. Tom은 아직도 제가 자기 드롭다운을 다 망가뜨렸다고 농담합니다. 하지만 저는 그보다 더 많이 망가뜨렸다고 생각하고 싶습니다.
제가 가장 좋아한 부분은 Prefetch라는 폴더였습니다. PrefetchApp, PrefetchSecurity, PrefetchOrderFlow, PrefetchOptionsChain, PrefetchPortfolio가 들어 있었습니다. 이 POC는 로딩 상태나 스켈레톤을 두지 않는 걸 원칙으로 삼았습니다. 페이지는 즉시 그려졌고, 돌아가는 스피너는 하나도 없었고, 새 디자인 언어는 한 번 읽으면 이해되는 단순한 컴포넌트 위에 얹혔습니다. 저희는 이걸 내부 테스트 링크에 올려서 사람들이 직접 경험해 보게 했습니다!
Tom이 거의 1:1로 그대로 구현한 Thiago의 새 아트 디렉션
이 브랜치는 1월에 한 줄짜리 메모와 함께 아카이브됐습니다. 여기서 배운 것들을 기존 웹 프로젝트에 녹여 넣고 있다는 내용이었습니다. 실제로 그렇게 됐습니다. 이후 몇 달에 걸쳐 배포된 ws26(Wealthsimple 2026) 화면들, 보유 종목 대시보드, 새 계좌 상세는 모두 그 브랜치까지 거슬러 올라갑니다. POC는 프로덕션 웹 앱에서 도달하고 싶은 기준이 됐습니다. 기준선을 세운 것입니다.
그다음 Thiago가 디자인 언어를 한 단계 더 밀고 나갔습니다. POC 이후에 그는 컴포넌트와 인터랙션을 모은 갤러리를 만들었습니다. 보유 종목 화면, AI로 거래하기 버튼, 지출 트리맵, 프라이버시 모드, 통화 전환기, 내비게이션 드로어 변형, 카드 애니메이션, 심지어 셰이더까지 있었습니다. 분명히 말하면 이건 Figma 파일이 아닙니다. 자기가 만든 컴포넌트와 인터랙션을 보여 주는 사이트를 직접 만들었습니다.
잠깐 곁길로 새는 이야기입니다. 이것도 AI가 일하는 방식을 바꿔 놓은 또 하나의 예입니다. Fey를 만들 때는 Tom과 제가 받는 디자인이 대부분 정적이었고, 만들면서 빈 곳을 저희가 채웠습니다. 지금은 AI 덕분에 Thiago가 제가 함께 일해 본 어떤 엔지니어보다 더 좋은 경험을 만들어 냅니다. 그가 만들어 내는 걸 보면 정말 놀랍습니다. 그는 그냥 최고입니다.
맛보기로, 그가 자기 사이트에서 만든 것 몇 개를 이 글에 옮겨 왔으니 직접 경험해 보시면 좋겠습니다. 여기 있는 것들은 이후 모두 프로덕션에 반영됐습니다.
https://performance.dev/wealthsimple-year-one#the-proof-of-concept
과거의 빚을 갚다
9월부터 12월까지 저는 웹 앱의 기반을 개선하는 일에 매달렸습니다. 해야 할 게 많았지만 누군가는 이 지저분한 일을 해야 한다는 걸 알고 있었습니다.
Angular 셸은 라우트 하나씩 떼어 냈습니다. 분명히 해 두자면 혼자 한 일이 아닙니다. Wealthsimple의 한 팀이 저희가 오기 훨씬 전부터 Angular 마이그레이션을 조금씩 깎아 내고 있었습니다. 라우트 분리, UI-Router 제거, 인터셉터 해체에는 제 이름이 아닌 이름들이 붙어 있었습니다. 제가 보탠 건 연료와 집념이었다고 생각합니다. 저는 지루한 방식으로 밀었습니다. 라우트 하나를 떼고, 확인하고, 다음으로 넘어갔습니다. 그중 분리 PR 하나는 17,221줄을 지웠습니다. 그 과정에서 인증 계층 전체(로그인, SAML, Okta, 2단계 인증)를 React로 옮겼습니다. 12월 3일에 마지막 PR이 들어갔습니다. 제목은 "The final reckoning of Angular Deletening"이었고, 남아 있던 1,776줄을 마지막으로 지웠습니다. 이제 앱은 위부터 아래까지 전부 React였습니다.
그사이 Tom은 첫 주에 시작한 파워 유저 기능을 계속 밀고 나갔습니다. 단축키는 제대로 된 시스템으로 자랐고, 미체결 주문 독은 앱 전체를 이동하는 법을 익혔습니다. 제 Linear 글을 읽으셨다면 제가 키보드를 주된 입력 수단으로 얼마나 신뢰하는지 아실 겁니다. Tom이 가을에 만든 그 시스템은 오늘도 모든 것의 밑에서 돌고 있습니다.
2025년 12월 3일, Angular 마이그레이션을 끝냈습니다
다시 디자인한 화면들
여기가 Thiago의 1년이 담긴 곳이고, 눈으로 볼 수 있는 부분입니다.
스크린샷을 보여 드리기 전에 한 가지는 솔직하게 말하고 싶습니다. 저는 10년 동안 Thiago 옆에서 일했고 그가 얼마나 뛰어난지 정확히 안다고 생각했습니다. 올해 저는 그를 더 크게 보게 됐습니다. 그는 화면만 다시 디자인한 게 아니라, Wealthsimple의 모든 사람이 화면의 모습과 감각에 기대하는 수준 자체를 끌어올렸습니다.
1월부터 앱은 새로운 방향으로 움직이기 시작했습니다. 시각적으로는 전면 개편이었습니다. 내비게이션 사이드바, 자산 유형 다섯 가지의 주문 흐름, 완전히 새로 만든 보유 종목 대시보드가 나왔습니다. 이어서 입출금 계좌와 신용카드 화면을 다시 만들었습니다.
마케팅 카드가 들어간 홈 사이드바
주문 흐름. Danny Williams에게 감사를 전합니다
새로운 지출 인사이트 사이드바
보유 종목 대시보드는 따로 이야기할 만합니다. Tom이 만들었고, POC의 가장 뚜렷한 후손입니다. 빽빽한 표, 전부 미리 받아 오는 로딩 방식, 그 밑에 깔린 키보드 단축키까지 그렇습니다. POC와 프로덕션 대시보드를 나란히 놓고 보면 거의 1:1이라는 게 보입니다.
보유 종목 화면은 내부에서도 큰 골칫거리였습니다. 빠르게 만드는 데 애를 먹고 있었고, 끊이지 않는 지원 티켓에 지쳐 가고 있었습니다. Tom이 처음부터 다시 만들어서 웹에서 가장 빠른 페이지 중 하나로 바꿔 놓은 게 정말 고맙습니다.
새 보유 종목 표 디자인의 진면목
최근에는 웹의 뱅킹 흐름이 개선됐습니다. 입출금 계좌는 미로처럼 얽혀 있던 흐름을 동작만 모아 놓은 카드 하나로 접었습니다. 추가하고, 이체하고, 납부하면 끝입니다. 신용카드에는 제대로 된 지갑이 붙었고, 모든 색상 조합으로 그려집니다. 지갑이 이렇게까지 예뻐야 할 이유는 아무에게도 없었습니다.
이것도 AI가 일하는 방식을 바꿔 놓은 좋은 예입니다. Tom이 뱅킹 팀과 바짝 붙어 이 기능들을 만들던 게 기억나는데, 페이지 하나를 하루 만에 끝내는 것처럼 보였습니다. 피드백은 거의 즉시 반영됐고, 디자인보다 앞서가는 일도 많았습니다.
AI 이전이라면 이런 페이지는 만드는 데 몇 주가 걸렸을 겁니다. 지금은 Thiago가 인터랙션까지 만들어 디자인을 넘기면, Tom이 며칠 안에 잘 다듬어진 페이지로 바꿔 놓습니다. 그리고 이건 시작일 뿐입니다.
입출금 계좌 사이드바에 새 옷을 입혔습니다
카드 홀더, 여섯 가지 방식.
이 절에서 가져가셨으면 하는 건 속도입니다. 이건 2년을 들여 한 번에 공개하는 리디자인이 아니었습니다. 화면은 준비되는 대로 플래그 뒤에서 계속 배포됐습니다. 저희는 웹을 빠르게 만든 것만이 아닙니다. Wealthsimple이 무언가를 내보내는 속도 자체를 바꾸고 있었습니다.
수백 개의 작은 결정
사람들은 늘 저에게 앱을 어떻게 빠르게 만드는지 묻습니다. 디자인이 어떻게 이렇게 좋아 보이는지, 왜 이렇게 기분 좋게 느껴지는지도 묻습니다. 제 답은 항상 같습니다. 작은 결정 수백 개를 제대로 해낸 결과입니다. 무엇 하나를 어떻게 하는지가 모든 것을 어떻게 하는지를 말해 줍니다.
제가 가진 또 하나의 믿음은, 심리학 전공자로서 가장 좋아하는 것 중 하나인데, 깨진 유리창 이론입니다. 모르는 분을 위해 설명하면, 눈에 보이는 무질서의 흔적이 사람들로 하여금 무질서를 더 보태거나 그냥 넘기게 만드는 환경을 만든다는 생각입니다. 그래서 저는 디테일이 중요하다고 믿습니다. 한 번 미끄러지기 시작하면 눈덩이처럼 커집니다.
코드에서는 URL 설계, API 응답 본문, 스키마 설계, 순차 실행과 병렬 실행, 캐싱 전략, 페이지네이션, 에러 처리, 재시도 로직, 데이터베이스 쿼리, 인증, 상태 관리, 컴포넌트 경계, 프리페치, 의존성, 로깅, 검증에서 이게 보입니다. 모든 영역에는 저마다의 신호가 있습니다. 어디를 봐야 하는지 아는 사람에게는 모든 걸 알려 주는 단서입니다.
그래서 큰 과제를 진행하는 동안에도 저는 늘 사이드 퀘스트를 하나씩 돌렸습니다. 이런 디테일을 정리하는 일입니다. 최근에 가장 마음에 든 것 하나를 소개합니다.
회사 로고 개선하기
예전에는 모든 로고를 런타임에 GraphQL 쿼리로 받아 왔습니다. 그래서 누군가 활동 피드를 열면, 먼저 활동 내역을 요청하고, 그다음 항목마다 회사 로고를 가져오는 요청을 또 보냈습니다. 고통스러운 워터폴이었습니다.
게다가 로고는 스토리지 버킷 URL에서 그대로 내려오는 PNG였습니다. 저는 파이프라인을 벡터 중심으로 다시 만들고, 모든 종목에 추측할 수 있는 URL을 붙였습니다. 이제 로고는 어떤 크기에서도 선명하고, 전송량은 아주 작고, 영구적으로 캐시할 수 있으며, URL도 깔끔하고 단순합니다.
// 이전
ws-company-logo-cdn-bucket-prod.s3.amazonaws.com/logos_with_market/XNAS_AAPL.png
// 이후
logos.wealthsimple.com/aapl-xnas.svg그리고 당연히 워터폴 요청도 없앴습니다. 여러 번 요청하는 대신 이제 첫 요청에서 로고 URL을 바로 돌려줍니다.
# 이전: 한 줄마다 왕복 요청 두 번
query ActivityFeed {
activityItems { id title symbol }
}
query SecurityLogo($symbol: String!) {
securityLogo(symbol: $symbol) { url }
}
# 이후: 항목 안에 로고를 함께 담음
query ActivityFeed {
activityItems {
id
title
securityLogo { url } # svg URL, 즉시 렌더링된다
}
}초기 로딩 개선하기
앱은 네트워크 응답이 오기 전에, 로컬에 있는 정보만으로 최종 레이아웃을 즉시 그려야 합니다. 그래서 저희는 데이터만이 아니라 페이지의 모양을 캐시했습니다. 아바타가 아이콘에서 글자로 깜빡이지 않도록 사용자 프로필 이니셜을 캐시했고, 계좌 이름도 캐시했고, 심지어 스켈레톤 행 개수까지 캐시했습니다. 그래서 로딩 화면이 8개라는 어림짐작 대신 사용자가 실제로 보유한 종목 수와 맞아떨어집니다. 차트는 로딩이 끝난 뒤의 높이를 미리 잡아 둡니다. 값을 훑을 때는 고정폭 숫자를 써서 자릿수가 흔들리지 않습니다.
폰트 파일을 제대로 캐시하기
한 줄로 끝난 폰트 수정입니다. 폰트 파일은 [name].[ext] 형태로 나가면서 10년짜리 캐시 헤더를 달고 있었고, 예전부터 쓰던 ?v=N 방식 때문에 preload 태그와 @font-face 규칙이 서로 다른 URL을 요청해서 모든 폰트를 두 번씩 받아 왔습니다. 파일명에 콘텐츠 해시를 넣으니 둘 다 해결됐습니다. 한 줄이었습니다.
데이터를 똑똑하게 미리 받아 오기
프리페치를 어디에나, 조심스럽게 넣었습니다. 연말이 되자 거의 모든 인터랙티브 화면이 클릭했을 때 필요한 것을 미리 받아 왔습니다. 마우스를 올리면 라우트를, 데이터를, 이미지를 미리 받고, 세그먼트 컨트롤에 올리면 차트 데이터를 미리 받았습니다. 변수를 기준으로 만들어지는 캐시 키가 일관되게 유지되도록 하는 장치도 만들었습니다.
// 마우스를 올릴 때: 캐시를 미리 채운다
prefetchQuery(OrderTicket, { securityId, accountId });
// 클릭할 때: 똑같은 오퍼레이션에 똑같은 변수라서 즉시 뜬다
useQuery(OrderTicket, { variables: { securityId, accountId } });
// 어느 한쪽에 변수가 하나만 더 붙어도 캐시는 절대 맞지 않는다.
// 티켓을 열 때 실제로 무엇을 읽는지 점검한 덕분에
// 옵션 체인 호버 요청이 13개에서 5개로 줄었다.언마운트 문제 고치기
라우트 계층 이야기입니다. 앞에서 말한, 눈에 보이지 않는 리마운트를 기억하시나요? 모든 페이지에서 history.push를 할 때마다 라우트 서브트리 전체가 허물어지고 다시 만들어졌습니다. 레이지 라우트 래퍼가 React.lazy가 해결되는 동안 두 가지 JSX 모양을 왔다 갔다 했기 때문입니다. React는 같은 위치에서 다른 엘리먼트 타입을 봤고, 그건 언마운트하고 다시 마운트한다는 뜻입니다. 수정한 방식은 JSX 모양을 하나로 고정하고 저희 프리페치 캐시를 직접 읽는 것입니다. 이제 미리 받아 둔 라우트는 첫 렌더에서 동기적으로 그려집니다.
// 이전: React.lazy가 해결됐는지에 따라 JSX 모양이 두 가지로 갈린다.
// React는 화면을 이동할 때마다 같은 위치에서 새 엘리먼트 타입을
// 보게 된다. 그래서 언마운트하고 다시 마운트한다.
const Sync = getResolvedModule(loader)?.default ?? null;
const Lazy = useMemo(() => createLazy(loader), [loader]);
if (Sync) {
return (
<WidgetErrorBoundary Fallback={ErrorFallback}>
<Sync />
</WidgetErrorBoundary>
);
}
return (
<Suspense fallback={<Fallback />}>
<WidgetErrorBoundary Fallback={ErrorFallback}>
<Lazy />
</WidgetErrorBoundary>
</Suspense>
);
// 이후: 모양을 하나로 고정하고 우리가 가진 해결된 모듈 캐시를 읽는다.
// 정말로 로드되지 않은 라우트에서만 서스펜드한다.
// 미리 받아 둔 라우트는 첫 렌더에서 동기적으로 그려진다.
const PageLoader = ({ loader }) => {
const resolved = getResolvedModule(loader);
if (resolved) {
const Page = resolved.default;
return <Page />;
}
throw loadModule(loader); // 진행 중인 프로미스를 Suspense가 받는다
};
return (
<Suspense fallback={<Fallback />}>
<WidgetErrorBoundary Fallback={ErrorFallback}>
<PageLoader loader={loader} />
</WidgetErrorBoundary>
</Suspense>
);하나하나 떼어 놓으면 작은 것들입니다. 다 합치면 그냥 돌아가는 앱과 세심하게 만들어진 느낌을 주는 앱을 갈라놓습니다.
영향력을 키우기 위해 시스템을 만들다
저희 힘으로 모든 걸 다 할 수는 없다는 걸 알고 있었습니다. 큰 조직에서는 시스템으로 생각해야 합니다. 이곳에는 엔지니어와 디자이너가 수백 명 있습니다.
12월 말에 저는 테마 프로바이더를 처음으로 손봤습니다. Figma에서 나온 새 디자인 토큰을 쓰도록 테마를 바꾸는 거대한 풀 리퀘스트 하나였습니다. 새 방향의 출발점이었지만, 여기서 더 멀리 가는 무언가가 필요했습니다. 다음 단계는 기존 디자인 시스템인 Patchwork를 현대화하는 일이었습니다.
저희가 합류한 직후 Wealthsimple의 'Simple Sans'가 Futura를 대체했습니다
Mint 디자인 시스템
제 목표는 새 디자인 시스템을 Thiago의 개념 증명 위에 세우는 것이었습니다. 취향, 모션, 컴포넌트마다의 감각을 그가 이렇게 느껴져야 한다고 믿은 그대로 만드는 것입니다. 제 일의 상당 부분은 그 개념 증명을 조직 전체가 쓸 수 있는 시스템으로 바꾸는 것이었습니다. 가이드라인, 에이전트 스킬, Figma Code Connect, 문서, 마이그레이션 도구를 만들었습니다. 다시 말해 Thiago의 취향을 회사의 기본값으로 만드는 일이었습니다.
Thiago의 개념 증명을 가능한 곳마다 반영한 새 디자인 시스템 Mint를 살짝 보여 드립니다.
https://performance.dev/wealthsimple-year-one#mint-design-system
안을 들여다보면 Mint는 의도적으로 현대적이고 단순합니다. Base UI 위에 만들어서 접근성과 포커스, 키보드 동작이 직접 짠 코드가 아니라 검증된 프리미티브에서 나옵니다. 예전에는 Patchwork와 일회성 컴포넌트, 팀별 포크에 흩어져 있던 것들을 하나의 시스템으로 모았고, 토큰과 모션, 여백, 테마까지 전부 표준화해서 각 작업을 하는 방법이 하나만 남게 했습니다. 이 글에 나오는 다른 작업들과 나란히 만들었고, 마이그레이션이나 기능 개발을 한 번도 멈추지 않았습니다. 그리고 저희가 AI를 먼저 고려해 설계한 첫 시스템입니다. 프리미티브 수를 줄이고 단순하게 만들면서 강한 관례와 에이전트 스킬을 붙였기 때문에, 에이전트든 새로 온 엔지니어든 기본값만 따라도 올바른 인터페이스가 나옵니다. 기본값이 취향을 담고 있으니, 기본값 그대로도 아름답습니다.
그렇다고 Mint가 완전히 새로운 건 아닙니다! 기본적인 레이아웃과 여백은 상당 부분 기존 시스템인 Patchwork 위에 올렸습니다. 예전 시스템에서 배운 것들을 새 시스템 안으로 접어 넣었습니다.
Mint는 몇 주 만에 브랜치에서 회사 전체의 기본값이 됐습니다. 도구와 여러 차례의 마이그레이션 물결이 그 일부를 해냈습니다. 여기서도 AI가 한 걸음 더 나아갔다는 걸 느꼈습니다. 예전에는 디자인 시스템 하나에 몇 달과 팀 하나의 노력이 들었지만, 지금은 참고할 자료와 취향만 있으면 한 사람이 만들 수 있습니다.
AI에 더 크게 걸다
첫 1년 동안 Tom과 저는 둘이서 모노레포에 아주 많은 풀 리퀘스트를 머지했습니다. 어떻게 그럴 수 있었냐는 질문에 솔직하게 답하면 두 가지입니다. 배포하기 좋게 만들어진 시스템, 그리고 1년 동안 AI에 전부를 걸었다는 것입니다. 1년에 걸쳐 저희 기여가 계속 올라간 걸 볼 수 있습니다.
AI 자체도 저희 발밑에서 계속 바뀌었습니다. 합류할 때는 Cursor를 쓰고 있었고, 코드를 손으로 쓰는 일도 여전히 많았습니다. 1년에 걸쳐 저희는 Claude Code를 쓰기 시작했고, 모델은 계속 좋아졌습니다. 그러는 사이 일 자체가 바뀌었습니다. 저희는 코드 대부분을 직접 쓰는 걸 멈추고 에이전트를 관리하기 시작했습니다. 지금은 여러 개를 동시에 돌리면서 방향을 주고 피드백하는 데 시간을 씁니다. 실수가 충분히 드물어져서 그걸 고치는 일은 더 이상 병목이 아니기 때문입니다. 이제 병목은 무엇을 만들지 정하는 일입니다.
숫자로도 보입니다. 2026년 2월부터 저희 PR의 상당 비중이 Claude Code와 짝을 이뤄 작성됐고, 정확히 그 지점에서 두 사람의 처리량 곡선이 꺾여 올라갑니다. 전월 대비 대략 4배였습니다. 레버리지는 복리로 쌓입니다. 에이전트가 기계적인 중간 작업을 맡는 동안(마이그레이션 물결, 테스트 골격 만들기, 코드모드에서 빠진 잔여 항목, 죽은 코드 정리) 저희는 판단이 필요한 부분에 머무릅니다. 최근에 모바일에는 있지만 웹에는 없는 기능에 대한 지원 티켓이 들어왔습니다. 저는 웹에서 같은 기능을 한 시간쯤 만에 배포했습니다. 티켓이 들어오고 PR을 올리기까지 걸린 시간이 그 정도였습니다.
이게 가장 잘 작동할 때 어떤 모습인지 가장 분명하게 보여 주는 예가 Mint입니다. 보통이라면 전담 팀이 몇 달을 들여야 하는 디자인 시스템(토큰, 프리미티브, 문서, Figma 동기화, 마이그레이션 도구)이 골격에서 정식 출시까지 몇 주 만에 갔습니다. 에이전트가 기계적인 표면적을 만들고, 사람이 모든 디자인 결정을 내렸기 때문입니다. AI가 Mint를 디자인한 게 아닙니다. AI는 Mint를 만드는 비용을 충분히 낮춰서, 잘 디자인하는 일만 남게 했습니다.
그래도 무엇을 요청해야 하는지는 알아야 하고, 답이 틀렸는지, 혹은 맞았지만 좋지는 않은지 알아보려면 높은 수준의 취향이 필요합니다. 에이전트는 사람이 가져온 판단력을 그만큼 곱해 줍니다. 주도성과 취향에 보답하는, 어마어마한 레버리지를 가진 도구입니다.
Thiago의 새 아트 디렉션 다크 모드 버전. 저희가 향하는 북극성입니다!
더 큰 그림
1년 전 저희 세 사람은 스타트업을 운영하고 있었습니다. 오늘은 캐나다에서 가장 중요한 금융 회사 중 하나의 안에 있습니다. 그리고 제가 예상하지 못한 게 있습니다. 미션이 작아지지 않고 오히려 커졌습니다.
이 글에 나온 모든 것, 지워 낸 Angular 셸, 다시 디자인한 화면들, 디자인 시스템, 천 개의 작은 디테일은 하나의 목표를 향합니다. 저희는 세계 최고의 웹 앱과 모바일 앱을 만들고 싶습니다. 최고의 금융 앱이 아닙니다. 최고의 앱입니다. 기준은 Linear, ChatGPT, 그리고 제가 이 사이트에서 다루는 앱들입니다. 수백만 명의 캐나다인이 Wealthsimple로 자기 돈을 관리하고, 그들은 그 수준의 소프트웨어를 누릴 자격이 있습니다.
1년의 모양은 대략 이렇습니다. AngularJS 셸은 라우트 하나씩 떨어져 나갔고, 그동안에도 앱은 계속 배포됐습니다. 디자인 시스템 두 개가 호환 이음새와 코드모드 물결, 린트 울타리의 형태로 들어왔고, 라인은 멈추지 않았습니다. Tom의 POC는 설계도가 됐고, Thiago의 디자인은 기준이 됐고, 화면 하나하나가 바뀌면서 앱은 하나의 제품처럼 보이기 시작했습니다. 그 밑에는 작은 결정 수백 개가 깔려 있습니다. 아예 받아 오지 않는 로고, 제대로 캐시되는 폰트, 프리페치 캐시에서 동기적으로 그려지는 라우트 같은 것들입니다. 그리고 이 모든 것을 관통하는 게 에이전트입니다. 에이전트는 깔끔해진 코드베이스를 복리로 쌓이는 속도로 바꿔 놨습니다.
1년이 지났고, 기반이 이제 모양을 잡아 가고 있습니다.
무엇보다 AI가 판을 바꾸고 있습니다. 지금은 만드는 사람이 정말로 만들 수 있는 단계입니다. 비전을 가진 사람이 그걸 실행할 수 있는 단계입니다. 2년 차에는 훌륭한 소프트웨어를 만드는 방식 자체가 바뀔 거라고 생각하고, 저는 정말 설렙니다.
이 이야기 중 어느 하나라도 마음에 닿았다면, 대부분이 눈치채지 못하는 디테일을 알아보는 사람이라면, Thiago와 Tom, 그리고 저와 함께해 주세요. X에서 저에게 DM을 보내 주세요. 진짜는 진짜를 알아봅니다.