리액트를 잘하면 좋은 엔지니어가 될 줄 알았다
측정하기 쉬운 React의 깊이를 엔지니어로서의 성장이라고 믿었던 시간
측정하기 쉬운 깊이를 성장이라고 믿었던 시간
한동안 나는 공부할 목록의 아래쪽으로 내려가는 방식으로 성장했다.
처음에는 컴포넌트를 잘 나누고 싶었다. 그러다 상태를 어디에 두어야 하는지 궁금해졌고, 상태가 바뀔 때 어떤 컴포넌트가 다시 렌더링되는지를 공부했다. 렌더링을 따라가다 보니 reconciliation이 나왔고, 그 아래에는 Fiber와 scheduler, lane이 있었다. 하나를 이해하면 그다음에 모르는 것이 자연스럽게 나타났다.
어제는 설명하지 못했던 동작을 오늘 설명할 수 있게 됐고, 읽히지 않던 코드를 조금 더 따라갈 수 있게 됐다. 모르는 것이 줄어드는 만큼 내가 좋은 엔지니어에 가까워지고 있다는 감각도 생겼다.
좋은 엔지니어가 됐는지를 확인하는 일은 그보다 훨씬 어려웠다.
제품의 문제를 제대로 정의했는지, 복잡한 요구를 적절한 구조로 옮겼는지, 내가 만든 것이 사용자에게 실제로 도움이 됐는지, 동료가 이후의 변경을 더 쉽게 할 수 있게 됐는지는 혼자 공부한 시간만큼 곧바로 점수로 돌아오지 않았다. 좋은 문제와 판단을 실행할 권한, 사용자 반응을 확인할 데이터가 필요했다. 결과가 나타나더라도 그것이 내 판단 덕분인지, 팀과 시장과 시기의 영향인지 분리하기 어려웠다.
React는 이보다 친절한 성적표였다. 내가 무엇을 모르는지 비교적 명확했고, 다음에 공부할 것도 쉽게 찾을 수 있었다. 누군가 중요한 문제를 맡겨주지 않아도 문서와 소스 코드만 있으면 혼자 더 깊이 들어갈 수 있었으며, 제품의 결과가 좋지 않더라도 새로 이해한 개념은 남았다.
나는 어느 순간부터 좋은 엔지니어가 됐는지를 직접 확인하는 대신, React를 얼마나 깊이 아는지를 측정하기 시작했다. 측정하기 어려운 엔지니어링 실력 대신, 측정하기 쉬운 프레임워크 숙련도를 성장의 대리 지표로 사용하고 있었다.
내가 혼자 채점할 수 있었던 성장

React 공부에는 계단이 있는 것처럼 보였다.
useEffect의 실행 시점을 이해하고 나면 의존성 배열이 보였고, render와 commit을 구분한 뒤에는 Fiber가 궁금해졌다. Fiber를 읽다 보면 scheduler가 나왔고, scheduler를 이해하려면 update priority와 lane을 알아야 했다. 하나의 개념이 자연스럽게 더 아래의 개념을 가리켰기 때문에, 깊어지는 것과 앞으로 나아가는 것을 구분하기 어려웠다.
제품 개발은 그렇게 다음 단계를 알려주지 않았다. 사용자가 왜 기능을 쓰지 않는지는 React 문서에 적혀 있지 않았고, 요구사항에서 빠진 전제를 알려주는 타입도 없었다. 지금 빠르게 실험해야 하는지, 구조를 다시 만들어야 하는지, 반복되는 문제가 코드의 복잡성 때문인지 책임과 권한이 나뉜 방식 때문인지 판단하려면 다른 종류의 관찰이 필요했다.
React를 공부할 때는 부족함을 설명하기 쉬웠다. 모르는 개념이 있었고, 그것을 공부하면 됐다. 반면 제품 문제 앞에서는 무엇을 모르는지조차 모르는 경우가 많았다.
나는 불확실한 문제를 다루는 대신 공부한 만큼 깊어지는 문제를 선택했다. 어려운 개념을 이해하기 위해 많은 시간과 집중력을 들였으므로 그 성취가 거짓이었던 것은 아니다. 다만 어려운 공부를 하고 있다는 사실과 필요한 문제를 풀고 있다는 사실은 같지 않았다.
그럼에도 더 깊은 지식이 언젠가 더 큰 문제를 해결하는 능력으로 자연스럽게 바뀔 것이라고 생각했다. Fiber를 이해하면 복잡한 렌더링 문제도 풀 수 있을 것 같았고, scheduler와 lane을 알면 제품에서도 더 나은 성능 판단을 내릴 수 있을 것 같았다.
지식은 실제로 선택지를 늘려줬다. 하지만 선택지가 많아진다고 지금 어떤 선택을 해야 하는지까지 저절로 알게 되는 것은 아니었다.
렌더링 과정을 정확히 이해하더라도 현재의 병목이 렌더링인지, 네트워크인지, 데이터 모델인지부터 구분해야 했다. 상태 관리 패턴을 많이 알더라도 어떤 사실을 URL에 두고, 무엇을 서버에서 관리하며, 어떤 값을 컴포넌트에 남겨야 하는지는 별도로 판단해야 했다.
오히려 익숙한 지식이 늘어날수록 제품의 문제를 내가 잘 아는 기술의 언어로 좁히기도 했다. 상태가 복잡하면 상태 관리 구조를 먼저 의심했고, 렌더링이 많으면 memoization을 떠올렸으며, 코드가 길면 컴포넌트를 분리할 방법부터 찾았다.
제품의 문제를 이전보다 정확하게 정의한 날보다 Fiber의 동작을 새로 이해한 날을 성장으로 인정하기 쉬웠다. 사용자 반응을 보고 가정을 버린 경험보다 복잡한 렌더링 원리를 설명할 수 있게 된 일을 더 기술적인 성취로 여겼다.
측정하기 쉬운 것이 목표를 대신하면 성적표는 선명해진다. 대신 그 성적표에 찍히지 않는 능력은 성장의 범위에서 조용히 사라진다.
Flame Chart에서 질문이 바뀌었다

React 내부에 대한 지식이 실제 판단에 필요했던 순간도 있었다.
토스에서 디자인 시스템과 관련된 제품을 개발할 때였다. 성능을 확인하기 위해 Performance Flame Chart를 살펴보다가 MobX와 관련된 작업이 상당한 영역을 차지하고 있는 장면을 봤다.
특정 라이브러리의 이름이 크게 나타났다는 사실만으로 그 라이브러리가 병목이라고 단정할 수는 없었다. 먼저 관찰한 비용을 세 층으로 나눠봐야 했다.
| 관찰 지점 | 확인할 질문 |
|---|---|
MobX reaction | reaction 자체가 오래 걸리는가 |
observable → observer | 하나의 변경이 지나치게 넓은 범위를 깨우는가 |
React render | 그 아래에서 실제 렌더링이 오래 걸리는가 |
당시에는 MobX 기반 상태를 Jotai의 atom 모델로 옮기는 선택지도 검토하고 있었다. 사용법만 비교한다면 observable과 reaction을 어떤 atom과 derived atom으로 나눌지, 구독 범위를 얼마나 작게 만들 수 있을지, 기존 코드를 옮기는 비용은 어느 정도인지 살펴볼 수 있었다.
하지만 바꾸려는 것이 단지 상태를 작성하는 방식인지, 상태 변경이 React에 전달되고 처리되는 경로인지에 따라 비교 기준은 달라졌다.
- 하나의 변경이 깨우는
reaction이나 atom의 범위가 좁아지는가. - 계산과 실제 render cost가 줄어드는가.
- 업데이트를 non-blocking Transition으로 처리할 수 있는가.
이 차이는 라이브러리의 사용법만 읽어서는 알기 어려웠다. 상태를 React에 연결하는 구현까지 확인해야 했다.
당시 살펴본 mobx-react의 기능 컴포넌트 observer 경로는 mobx-react-lite를 통해 useSyncExternalStore를 사용하고 있었다.
useSyncExternalStore는 React 바깥에 존재하는 store의 값을 일관되게 구독하기 위한 API다. React 공식 문서에 따르면 non-blocking Transition을 처리하는 동안 외부 store의 snapshot이 달라지면 React는 화면 전체가 같은 버전의 값을 표시하도록 해당 작업을 blocking update로 다시 수행한다.
외부 store의 mutation 자체를 React가 소유한 state update처럼 non-blocking Transition으로 표시할 수 없다는 제약도 따른다.
반면 당시 검토한 Jotai v2의 useAtomValue는 useSyncExternalStore를 사용하지 않았다. atom의 변경을 구독한 뒤 React의 useReducer를 통해 다시 렌더링을 요청했다. Jotai로 옮기는 일은 상태를 나누는 방식뿐 아니라, 업데이트가 React에 들어가는 경로까지 바꿀 수 있는 선택이었다.
| 비교 지점 | MobX + mobx-react | Jotai |
|---|---|---|
| 상태의 단위 | observable 의존성을 reaction이 추적 | atom과 derived atom을 조합 |
| React 업데이트 경로 | observer → useSyncExternalStore | store.sub → useReducer |
| 확인할 성질 | reaction과 observer의 범위, render cost | atom의 경계, Transition 동작, render cost |
그렇다고 Jotai가 더 빠르다는 결론이 곧바로 따라오는 것은 아니었다. 업데이트 경로가 달라질 가능성과 실제 화면의 계산량이 줄어드는지는 별개의 문제였다. atom을 어떻게 나누는지에 따라 구독 범위와 계산 비용은 다시 달라질 수 있었고, 기존 상태 모델을 옮기는 비용도 함께 따져야 했다.
처음의 질문은 쉽게 MobX보다 Jotai가 빠른가로 좁아질 수 있었다. 내부 구현을 확인하고 나니 그보다 먼저 나눠야 할 질문이 보였다.
- 지금 관찰한 비용은 어디에서 발생하는가.
- Jotai로 옮기면 상태의 단위와 React 업데이트 경로 가운데 무엇이 달라지는가.
- 그 차이가 실제 화면의 Transition 동작과 render cost를 바꾸는가.
이때 React 내부를 읽는 일은 성장의 증명도, 지적 호기심을 만족시키는 공부도 아니었다. 라이브러리의 이름만 바꾸고도 문제가 해결됐다고 착각하지 않기 위해 필요한 확인이었다.
같은 깊이였지만 순서가 달랐다

겉으로 보면 이전에 하던 공부와 크게 다르지 않았다. 다시 React의 scheduling을 확인했고, 상태 관리 라이브러리가 업데이트를 React에 전달하는 방식을 살펴봤으며, 각 라이브러리의 Hook 구현을 읽었다.
달라진 것은 질문의 순서였다.
예전에는 내부 구현을 먼저 공부한 뒤, 언젠가 그 지식이 필요한 문제가 나타나기를 기다렸다.
이번에는 실제 비용을 관찰하고 결정을 내려야 했기 때문에, 필요한 만큼 내부로 내려갔다.
둘 다 React 내부를 보는 일이었다. 하나는 깊이를 먼저 정한 공부였고, 다른 하나는 판단에 필요한 깊이를 찾아간 조사였다.
내부 구현을 공부한 일 자체가 잘못은 아니었다. 그 지식이 있었기 때문에 Flame Chart에 MobX가 넓게 보인다는 사실을 곧바로 원인이라고 부르지 않고, reaction과 subscription, 실제 render cost를 구분해서 볼 수 있었다. Jotai의 atom 모델이 더 단순해 보인다는 이유만으로 더 나은 선택이라고 결론 내리지 않고, mobx-react와 Jotai가 각각 업데이트를 React에 전달하는 경로까지 확인할 수도 있었다.
문제는 내부 구현이 필요한 질문을 갖기 전에 깊이 내려가는 것 자체를 성장이라고 믿었던 데 있었다.
같은 지식이라도 질문 없이 쌓였을 때는 내가 전진하고 있다는 감각을 주는 데 머물렀고, 실제 문제 뒤에 놓였을 때는 선택지의 차이를 설명하는 근거가 됐다. 깊은 지식의 가치가 달라진 것이 아니라, 깊이를 사용하는 순서가 달라진 것이다.
깊이는 방향이 아니라 해상도였다

한동안 나는 React 내부를 많이 알수록 더 좋은 엔지니어에 가까워진다고 생각했다. 지금은 얼마나 깊이 아는지보다, 현재 내리려는 결정에 추상화가 감춘 성질이 중요한지를 먼저 묻는다.
대부분의 제품 개발에서는 React의 public API로 충분하다. 버튼의 상태를 바꾸고 서버 데이터를 읽으며 form을 처리할 때마다 Fiber tree와 lane을 떠올릴 필요는 없다. 추상화는 내부 구현을 매번 의식하지 않고도 같은 계약 위에서 일할 수 있게 만들기 위해 존재한다.
하지만 추상화가 감춘 성질이 선택의 기준이 되는 순간도 있다. 업데이트를 중단하거나 낮은 우선순위로 미룰 수 있는지, 외부 store의 변경이 어느 범위에 어떤 방식으로 전파되는지, 라이브러리를 교체하면 subscription의 모양만 달라지는지 scheduling 특성까지 달라지는지를 판단해야 한다면 public API 아래로 내려가야 한다.
그때도 목표는 가능한 한 깊이 들어가는 것이 아니다. 다음 세 가지를 설명할 수 있을 때까지 내려가면 된다.
- 관찰한 현상을 어떤 원인으로 나눌 수 있는가.
- 후보 선택지 사이에서 실제로 달라지는 성질은 무엇인가.
- 변경한 결과를 어떻게 검증할 것인가.
반대로 현재의 판단에 필요한 답을 이미 얻었는데도 더 아래의 구현을 계속 따라가는 일은 좋은 공부일 수는 있어도, 지금의 문제를 해결하는 데 필요한 엔지니어링은 아닐 수 있다.
그렇다고 모든 공부가 당장의 문제에서 출발해야 한다고 생각하지는 않는다. 실제 문제가 생기기 전에 쌓은 지식 덕분에 무엇을 의심해야 할지 더 빨리 떠올릴 수 있었고, 필요한 구현까지 내려가는 비용도 줄었다. 당장 쓰임이 없는 공부가 이전에는 보이지 않던 문제를 알아보는 어휘를 만들어주기도 한다.
다만 그런 공부를 곧바로 엔지니어링 실력 전체의 점수로 환산하지 않으려 한다. 깊은 지식은 좋은 판단을 가능하게 하는 준비일 수 있지만, 그 자체가 좋은 판단의 증거는 아니기 때문이다.
깊이는 실력의 방향이라기보다 문제를 보기 위해 조절하는 해상도에 가까웠다. 너무 얕으면 추상화가 감춘 비용을 놓치지만, 필요 이상으로 깊어지면 처음에 풀려던 제품의 질문이 구현의 세부사항 사이에서 흐려진다.
여전히 좋은 엔지니어는 모호하다

React를 얼마나 아는지는 비교적 쉽게 드러났다. 반면 실제 엔지니어링에서 중요했던 능력 가운데 상당수는 내가 사용하던 성적표로 측정하기 어려웠다.
- 관찰한 현상을 곧바로 원인으로 부르지 않는 일.
- 익숙한 기술의 언어로 문제를 성급하게 좁히지 않는 일.
- 라이브러리의 사용법과 실제 실행 특성의 차이를 구분하는 일.
- 변경한 뒤 무엇을 측정해야 판단을 회수할 수 있는지 정하는 일.
- 현재의 판단이 틀렸을 때 돌아올 지점을 남기는 일.
이런 능력은 Fiber의 구조를 설명하는 시험에서는 드러나지 않는다. 하지만 어떤 문제를 먼저 풀지 정하고, 기술 지식을 어디에 사용할지 결정하는 순간에는 오히려 이런 판단이 먼저 필요했다.
React 내부를 아는 능력과 언제 그 내부를 봐야 하는지를 아는 능력은 달랐다.
전자는 문서와 코드를 공부하며 쌓을 수 있었다. 후자는 문제를 잘못 짚어본 경험과 실제 결과를 확인하는 과정에서 조금씩 생겼다. 나는 오랫동안 전자의 성장만 더 쉽게 인정했다.
이 글을 쓰고도 좋은 엔지니어가 무엇인지 선명하게 정의할 수는 없다. 가장 깊이 아는 사람이라고만 말할 수도 없고, 내부 구현은 몰라도 제품만 잘 만들면 된다고 말하고 싶지도 않다.
다만 이전보다 분명해진 것은 있다. 어려운 내용을 이해했다는 사실은 성장의 증거가 될 수 있지만, 내가 올바른 문제를 다루고 있다는 증거까지 되지는 않는다.
그래서 지금은 공부를 시작할 때 예전과 다른 질문을 두려고 한다.
- 내가 지금 결정하려는 것은 무엇인가.
- 현재 알고 있는 내용만으로 결정할 수 없는 이유는 무엇인가.
- 추상화가 숨기고 있는 어떤 성질을 확인해야 하는가.
- 어디까지 알면 선택지의 차이를 설명할 수 있는가.
- 그 판단이 맞았는지는 무엇으로 확인할 수 있는가.
이 질문이 없다고 공부가 무의미한 것은 아니다. 다만 그 공부가 호기심인지, 미래를 위한 준비인지, 현재 문제에 필요한 조사인지는 구분할 수 있어야 한다. 셋은 모두 가치가 있지만 같은 성적표에 놓이지는 않는다.
React를 깊게 공부했던 시간을 후회하지 않는다. 그 지식이 있었기 때문에 실제 문제가 나타났을 때 사용법의 비교에서 멈추지 않고, 상태 변경이 React에 전달되는 방식과 scheduling의 제약까지 확인할 수 있었다.
다만 깊이가 나를 올바른 문제로 데려가주지는 않았다. 무엇을 문제로 볼지, 관찰한 현상을 어떻게 나눌지, 현재 결정에 어떤 지식이 필요한지, 결과를 무엇으로 검증할지는 별도의 판단이었다.
예전에는 React를 잘하면 좋은 엔지니어가 될 것이라고 생각했다.
지금은 좋은 엔지니어링을 하다 보면 React 내부까지 내려가야 하는 순간이 온다고 생각한다.
나는 그 순서를 반대로 알고 있었다.
이 글에서 더 궁금한 점이 있나요?
민제님이 공개한 글과 프로젝트, 프로필을 보고 답해요.