직접 만들어보고 나서야 이해한 것들
과제의 제약을 따라가며 무엇을 배웠고 어디서 막혔는지를 정리한 회고다.
지난 글에서는 Virtual DOM과 Diff 알고리즘을 직접 구현하면서 React의 내부 원리를 따라가 봤다. 이번 글은 그 연장선이지만, 초점을 조금 바꿔보려 한다. 이번 과제를 다시 돌아보니 내가 더 크게 배운 것은 특정 기능의 구현 방식 자체보다도, 과제가 걸어 둔 제약조건이 왜 그렇게 설계되어 있었는가였다. Hook은 최상위에서만 써야 하고, state는 루트 컴포넌트에서만 관리해야 하며, 자식 컴포넌트는 props만 받는 stateless 함수로 유지해야 했다. 처음에는 다소 인위적인 제한처럼 보였지만, 구현을 마치고 보니 이 조건들이야말로 React의 설계 원리를 가장 강하게 드러내는 장치였다. [Source](https://cedis.tistory.com/105)
이번 과제는 기능 구현 과제이기도 했지만, 실제로는 상태의 책임을 어디에 둘지, 렌더링을 어떤 구조로 다시 연결할지를 배우게 만드는 설계 제약 과제에 더 가까웠다.
과제 조건을 다시 읽으면서
과제 공지에서 요구한 것은 단순히 useState, useEffect, useMemo를 흉내 내는 일이 아니었다. 함수형 컴포넌트를 감싸는 FunctionComponent 클래스를 만들고, 루트 컴포넌트만 state를 가지며, 자식은 props만 받는 순수 함수로 유지하고, Virtual DOM + Diff + Patch 흐름 위에서 실제로 동작하는 화면까지 만들어야 했다. 여기에 batching 고민, 테스트, 실제 React와의 차이 비교까지 붙는다. 처음에는 기능 목록처럼 보였는데, 구현해보니 전부 설계 제약이었다.
| 과제 제약 | 실제 구현에서 맞닥뜨린 문제 | 배운 점 |
|---|---|---|
| 루트 컴포넌트만 state 관리 | UI 분리와 상태 분리가 다른 문제라는 점이 드러남 | state는 편한 곳이 아니라 책임이 모이는 곳에 둬야 한다 |
| 자식은 stateless, props-only | props 설계가 곧 컴포넌트 경계 설계가 됨 | Lifting State Up을 코드로 체감하게 됨 |
| Hook은 최상위에서만 사용 | 호출 순서가 바뀌면 상태 슬롯이 깨짐 | Hook 규칙은 스타일이 아니라 구조적 제약이었다 |
| setState 후 자동 재렌더 | 값 변경만으로는 부족하고 렌더 예약이 필요함 | setState는 상태 변경 함수이면서 스케줄링 인터페이스다 |
| useEffect / cleanup | effect 실행보다 cleanup 타이밍이 더 어려웠음 | side effect는 허용보다 회수가 더 중요하다 |
| useMemo | deps 비교가 맞지 않으면 캐시가 무의미해짐 | 최적화도 유지 비용이 있는 기능이다 |
| Virtual DOM + Diff + Patch | 어디를 같은 노드로 볼지 정의하는 문제가 생김 | Diff는 비교 알고리즘이면서 identity를 정하는 규칙이다 |
| Batching 고민 | 여러 state 변경을 바로 반영하면 비효율적임 | 업데이트는 모아서 처리할수록 렌더 구조가 선명해진다 |
1. state를 루트에만 두라는 조건 덕분에 props 흐름을 처음 제대로 봤다
이번 과제에서 가장 먼저 체감한 건, UI를 컴포넌트로 나누는 일과 state를 어디에 둘지 정하는 일이 전혀 다른 문제라는 점이었다. 화면 조각은 비교적 쉽게 나눌 수 있다. 하지만 state는 어디에 둬야 가장 설명 가능하고, 어디에 둬야 다른 컴포넌트와 충돌하지 않는지가 더 어려웠다. 과제는 이걸 아예 강제로 제한했다. state는 루트 컴포넌트에서만 관리하고, 자식은 props만 받는 stateless 함수로 만들라는 조건 덕분에, 상태를 편한 곳에 흩뿌리는 대신 책임이 모이는 한 곳으로 끌어올리는 방식을 체감하게 됐다.
실제 저장소 설명에서도 RootApp가 theme, running, elapsedMs, startedAtMs, laps를 들고 있고, ThemeToggle, StopwatchCard, TimeSlots, LapsPanel 같은 하위 컴포넌트는 props 기반 UI로 설명된다. 구현을 마치고 나니 이 제약은 불편함이 아니라, state의 소유권과 데이터 흐름을 더 선명하게 보게 만드는 장치에 가까웠다. [Source](https://github.com/Jungle-12-303/week5-team3-react2)

2. Hook의 핵심은 API가 아니라 호출 순서 보존이었다
과제 공지에서 가장 중요한 질문 중 하나는 “함수는 매번 새로 실행되는데, 상태는 어떻게 유지할까?”였다. 이 문장을 실제로 구현해보니, Hook의 본질은 편리한 함수형 API가 아니라 호출 순서를 보존해 상태 저장 슬롯과 연결하는 규칙에 있었다. 함수형 컴포넌트는 렌더링될 때마다 다시 실행되는데, 이전 상태를 이어받으려면 이번 호출의 첫 번째 Hook이 지난번 첫 번째 Hook과 정확히 같은 자리를 가리켜야 한다.
저장소 설명에도 currentInstance와 hookIndex를 통해 hooks를 관리했다고 정리돼 있다. 이 구현을 직접 다뤄보니, 왜 실제 React가 Hook을 조건문 안에서 호출하지 말라고 하는지 뒤늦게 납득됐다. 그 규칙은 문법 취향이 아니라, 호출 순서가 바뀌는 순간 상태 저장 구조 자체가 깨지기 때문이었다. [Source](https://github.com/Jungle-12-303/week5-team3-react2)
Hook은 “함수형 컴포넌트에서 상태를 쓸 수 있게 하는 편한 도구”이기 전에, 상태 저장 위치를 호출 순서에 매핑하는 규칙 세트였다.
3. setState는 값 변경 함수가 아니라 렌더 예약 함수였다
과제 중간부터는 setState를 단순한 값 변경 함수로 보면 설명이 안 되는 순간이 왔다. 상태 값만 바꾸고 끝나면 화면은 바뀌지 않는다. 결국 setState는 “무엇이 바뀌었는가”만이 아니라 “언제 다시 렌더링할 것인가”까지 연결해줘야 했다. 그래서 setState의 책임은 데이터 대입이 아니라, 상태 변경과 재렌더 사이를 이어주는 인터페이스에 더 가까웠다.
저장소 README에서도 setState가 instance.scheduleUpdate()를 호출하고, queueMicrotask()로 여러 업데이트를 모아 처리하는 흐름이 정리돼 있다. 이 구조를 넣고 나서야 batching이 왜 필요한지도 감이 왔다. 상태가 여러 번 연속 바뀔 때마다 즉시 렌더링하면 비효율적이기 때문에, 업데이트를 모아서 한 번에 처리하는 구조가 필요했던 것이다. [Source](https://github.com/Jungle-12-303/week5-team3-react2)
4. useEffect에서 진짜 어려운 건 실행보다 cleanup 타이밍이었다
구현 전에는 useEffect를 “렌더 뒤에 실행되는 함수” 정도로 생각했다. 하지만 실제로는 effect를 실행하는 것보다, 이전 effect를 언제 정리하고 어떤 조건에서 다시 실행할지를 결정하는 편이 더 어려웠다. 특히 타이머처럼 계속 살아 있는 side effect는 cleanup이 조금만 어긋나도 바로 동작이 꼬인다.
README에도 interval cleanup, document title 변경, theme 반영 같은 effect 예시가 정리돼 있다. 이런 예시를 다뤄보니 useEffect는 생명주기를 흉내 내는 장치라기보다, side effect를 허용하면서 동시에 회수하는 구조에 가까웠다. 결국 effect의 어려움은 “무언가를 실행시킨다”가 아니라 “언제 정리할지까지 포함해 lifecycle을 정의한다”는 점에 있었다. [Source](https://github.com/Jungle-12-303/week5-team3-react2)
5. useMemo를 구현하면서 최적화도 유지 비용이 있다는 걸 배웠다
useMemo는 처음엔 비교적 쉬워 보였다. 값 계산 결과를 저장해 두고, 의존성이 바뀌지 않으면 재사용하면 되는 것처럼 보였기 때문이다. 하지만 실제로 구현해보니 어려운 건 캐시 자체보다 언제 캐시를 버릴지 판단하는 기준이었다. deps 비교가 조금만 흔들려도 캐시는 의미를 잃는다.
저장소에서는 fastest lap, slowest lap 계산에 useMemo를 활용했다고 정리돼 있다. 이런 식의 계산을 다뤄보면서 느낀 건, 최적화는 공짜가 아니라는 점이었다. 계산 비용을 줄이는 대신 의존성 관리 비용이 생기고, 코드를 읽는 사람은 이 캐시가 언제 유효한지까지 계속 추적해야 한다. 결국 memoization은 무조건 넣는 기술이 아니라, 비용을 이해하고 선택해야 하는 기능이라는 걸 배웠다. [Source](https://github.com/Jungle-12-303/week5-team3-react2)
6. Diff는 비교 알고리즘이면서 동시에 identity를 정하는 규칙이었다
이전 글에서도 Virtual DOM과 Diff 알고리즘 자체를 중점적으로 정리했지만, 이번 과제 기준으로 다시 돌아보면 진짜 어려움은 트리를 만든 뒤에 시작됐다. Virtual DOM 생성은 출발점이었고, 그 다음 문제는 “무엇이 같은 노드인지, 어디서부터 달라졌다고 봐야 하는지”를 결정하는 일이었다. 즉 Diff는 단순히 차이를 찾는 알고리즘이 아니라, 노드의 정체성을 어디까지 유지할지 정의하는 규칙이었다. [Source](https://cedis.tistory.com/105)
저장소 테스트 설명에도 text/attribute patch, child node 생성·삭제, keyed reorder가 포함돼 있다. 특히 keyed reorder를 직접 다뤄보면 key가 왜 필요한지 바로 드러난다. key는 단순한 식별자가 아니라, “이 노드를 이전 것과 같은 존재로 볼 수 있는가”를 결정하는 기준이기 때문이다. Patch는 DOM 조작 코드 자체보다, Diff가 내린 판단을 실제 화면에 집행하는 계층에 더 가까웠다. [Source](https://github.com/Jungle-12-303/week5-team3-react2)

7. 실제 React와 비교할수록, 우리가 만든 것은 핵심 구조의 축약본에 가까웠다
과제의 마지막 중점 포인트 중 하나는 “이번 주에 구현한 React와 실제 React의 차이점에 대해 공부해 보라”는 것이었다. 구현을 마치고 보니 이 질문이 왜 중요한지도 알게 됐다. 우리가 만든 것은 React의 복제품이라기보다, 그 중 핵심 축을 드러내기 위해 의도적으로 단순화한 모델에 가까웠다.
이전 글에서도 이번 구현은 pre-Fiber React 엔진 수준이라고 정리했고, 저장소 README 역시 실제 React는 update queue priority, render/commit phase 분리, Fiber 기반 concurrent rendering 같은 구조를 가진다고 비교한다. 오히려 이런 차이를 알고 나니, 우리가 만든 단순화된 구조 덕분에 React의 핵심 책임 분리를 더 선명하게 볼 수 있었다. 복잡한 스케줄링과 우선순위를 걷어내고 나서야 “상태 저장”, “렌더 예약”, “효과 정리”, “비교와 패치”가 각각 어떤 역할을 맡는지 보였기 때문이다. [Source](https://cedis.tistory.com/105) [Source](https://github.com/Jungle-12-303/week5-team3-react2)
정리: 이번 과제에서 배운 점 / 어려웠던 점
| 구분 | 내용 |
|---|---|
| 가장 크게 배운 점 | React의 핵심은 API 표면보다 상태의 소유권, 호출 순서, 렌더 예약, effect 정리 같은 내부 규칙에 있었다. |
| 가장 어려웠던 점 | Hook의 사용 위치 제약과 cleanup 타이밍, 그리고 setState 이후 렌더를 어떤 시점에 모아서 처리할지 정하는 일이 특히 까다로웠다. |
| 과제의 의도라고 느낀 점 | 단순히 React 비슷한 것을 만드는 것이 아니라, 왜 실제 React가 그런 규칙을 강요하는지 몸으로 이해하게 만드는 데 있었다. |
| 다음에 다시 한다면 | Hook 저장 구조, update 스케줄링, effect cleanup 규칙을 먼저 글로 정의한 뒤 구현에 들어갈 것이다. |
마무리
이번 과제를 하면서 가장 크게 바뀐 건 React를 바라보는 시선이었다. 예전에는 Hook과 state를 함수형 컴포넌트에서 편하게 쓰게 해주는 기능 정도로 생각했는데, 직접 구현해보니 그 아래에는 훨씬 더 엄격한 규칙과 책임 분리가 있었다. 결국 이 과제의 진짜 어려움은 기능 개수가 아니라 제약조건에 있었다.
돌아보면 이번 과제는 React를 흉내 내는 과제라기보다, 상태는 왜 루트에 있어야 하는지, Hook은 왜 최상위에서만 호출해야 하는지, setState는 왜 렌더 예약까지 책임져야 하는지를 몸으로 배우게 하는 과제였다. 이전 글이 구현 원리를 따라가는 글이었다면, 이번 글은 그 원리들이 왜 그런 형태를 가지는지 과제 제약을 통해 되짚어 본 회고로 남을 것 같다. [Source](https://cedis.tistory.com/105)
참고 링크
'개발 > 프로젝트' 카테고리의 다른 글
| 크래프트 정글 × 바이브 프로젝트Mini SQL을 두 번 만들고 나서야보인 것들 (1) | 2026.04.16 |
|---|---|
| 크래프톤 정글 × 바이브 프로젝트Mini SQL을 두 번 만들고 나서야보인 것들 (1) | 2026.04.08 |
| 크래프톤 정글 × 수요코딩회"React를 구현하라" (0) | 2026.03.26 |
| 크래프톤 정글 × 수요코딩회 - 자체제작 Redis, 타겟팅 서비스 (0) | 2026.03.19 |
| 바이브코딩으로 하루 만에 팀 프로젝트 완성하기 — Clean Email 회고 (0) | 2026.03.12 |