BBAK-jun 님의 상세페이지[3팀 박준형] Chapter 1-3. React, Beyond the Basics

과제 체크포인트

배포 링크

기본과제

equalities

  • shallowEquals 구현 완료
  • deepEquals 구현 완료

hooks

  • useRef 구현 완료
  • useMemo 구현 완료
  • useCallback 구현 완료
  • useDeepMemo 구현 완료
  • useShallowState 구현 완료
  • useAutoCallback 구현 완료

High Order Components

  • memo 구현 완료
  • deepMemo 구현 완료

심화 과제

hooks

  • createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
  • useShallowSelector 구현
  • useStore 구현
  • useRouter 구현
  • useStorage 구현

context

  • ToastContext, ModalContext 개선

과제 셀프회고

이번 과제를 진행하고 새롭게 정리가된 저의 생각을 블로그 글로 정리했습니다

기술적 성장

프레임워크의 내부 동작 원리에 대한 깊은 이해

1주차에서는 JavaScript로 SPA를 직접 구현하면서 이벤트위임, 소프트네비게이팅, 상태관리 등 모던 프론트엔드 프레임워크의 핵심 개념들을 바닥부터 구현해보는 경험을 했습니다.

2주차에서는 vite의 esbuild option의 jsx factory 함수를 직접 만들어 가상돔을 구현하고 커스텀 jsx 파서를 제작했습니다. 특히 async function의 렌더링 처리 방식과 코어 렌더링 로직을 구현하며 동기적 렌더링 전이구조를 만들어보는 흥미로운 경험이었습니다.

3주차에서는 React의 훅 시스템과 얕은비교/깊은비교를 직접 구현하면서 diffing 알고리즘을 구현해보는 경험을 했습니다.

이러한 단계적 학습을 통해 React와 같은 프레임워크가 어떻게 동작하는지, 그리고 그 내부에서 일어나는 최적화 기법들을 직접 체험하며 프론트엔드 개발에 대한 근본적인 이해도를 크게 향상시킬 수 있었습니다.

자랑하고 싶은 코드

특별히 자랑하고 싶은 코드는 없습니다. 이번 과제는 명확한 답이 있는 과제였기 때문에 테스트코드를 보고 열심히 테스트를 통과하기 위해 코드를 작성해나가면 되는 구조였습니다. 비교적 답이 없는 이전 과제들과 달리 수월했습니다.

개선이 필요하다고 생각하는 코드

개선이 필요하다고 생각하는 특별한 코드는 없습니다. 이번 과제는 명확한 답이 있는 과제였기 때문에 테스트코드를 보고 열심히 테스트를 통과하기 위해 코드를 작성해나가면 되는 구조였습니다. 비교적 답이 없는 이전 과제들과 달리 수월했습니다.

학습 효과 분석

따로 제공해주신 학습 갈무리를 통해 평소에 보관해두었던 아티클들을 다시 읽어보고 생각을 정리할 수 있었습니다.

특히 좋은 코드는 무엇일까에 대해 이전부터 깊게 생각해봤었는데, 최근에 클린 아키텍처를 읽으면서 좋은 코드는 자유를 박탈하는 코드라고 생각하게 되었습니다. 자유를 박탈하여 예측 가능한 코드가 함께 작성하기 좋은 코드라고 생각하게 된 계기가 되었습니다.

리액트 렌더링 과정에 대한 깊은 이해 Virtual DOM, Diffing 알고리즘, Reconciliation 과정을 직접 구현해보면서 React가 어떻게 성능 최적화를 달성하는지 체험할 수 있었습니다. 특히 가상돔이 단순히 성능 개선 도구가 아니라 개발자가 UI 업데이트 로직을 직접 작성하지 않도록 하는 추상화 레이어라는 점을 깨달았습니다.

메모이제이션에 대한 철학적 접근 Dan Abramov와 Stefano J. Attardi의 메모이제이션 논쟁을 통해 성능 최적화의 본질에 대해 생각해볼 수 있었습니다. 메모이제이션이 잘못 설계된 컴포넌트 구조를 땜질하는 용도가 아니라, 올바른 컴포넌트 구조 설계가 우선되어야 한다는 관점을 정립했습니다.

Context의 본질에 대한 재정의 Context를 단순한 상태관리 도구로 보는 관점에서 벗어나 Dependency Injection Tool로 이해하게 되었습니다. Mark Erikson의 관점을 통해 Context가 컴포넌트의 관심사 범위를 제한하는 격리 레이어 역할을 한다는 것을 깨달았습니다.

이러한 학습 과정에서 Robert Cecil Martin의 "패러다임은 개발자의 권한을 박탈한다"는 말이 깊이 와닿았습니다. 좋은 코드란 개발자의 자유를 의도적으로 제한함으로써 예측 가능하고 함께 작성하기 좋은 구조를 강제하는 코드라는 철학을 갖게 되었습니다.

과제 피드백

AI와 함께하는 학습의 새로운 경험

이번 과제는 줌 인터넷 과제를 다시 한번 좋은 도구와 함께 재도전해본 의미 있는 경험이었습니다. AI를 통해 코드 분석을 더욱 편하게 할 수 있었고, 나의 생각과 지식을 다시 한번 체계적으로 정리할 수 있는 기회가 되었습니다.

과제 설계의 우수성

특히 인상적이었던 것은 과제의 구성 방식이었습니다.

답이 있는 과제에서는 추가로 답이 없는 주제로 본인의 생각을 펼쳐보라고 한 점이 매우 좋았습니다. 단순히 정해진 정답을 구현하는 것에서 그치지 않고, 그 과정에서 얻은 인사이트를 바탕으로 더 깊은 사고를 할 수 있도록 유도하는 설계였습니다.

반대로 답이 없는 과제에서는 본인 코드의 추상적인 개념을 구체화하여 설명하라는 과제들이 아주 인상적이었습니다. 막연한 아이디어나 감각적인 이해를 명확한 언어로 표현하도록 강제함으로써 진정한 이해에 도달할 수 있었습니다.

가장 인상 깊었던 도전

개인적인 소견으로는 답이 없는 과제들이 가장 어렵고 힘들었지만 제일 인상적이었습니다. 정해진 정답을 찾아가는 과정이 아니라, 나의 생각들을 직접 정리해가면서 구체화시켜가는 과정이었기 때문입니다.

이러한 과정을 통해 단순히 기술적 구현 능력뿐만 아니라, 개발자로서의 사고 체계와 철학을 정립할 수 있는 귀중한 기회가 되었습니다.

학습 갈무리

리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.

React와 같은 자바스크립트 라이브러리에서 사용 되는 개념인 Virtual DOM(가상돔)은 실제 DOM(Document Object Model)과 비슷한 구조를 가지지만, 자바스크립트 엔진 메모리에 상주하며 브라우저에서 실제로 표시되는 돔과는 독립적으로 존재합니다.

가상돔이 존재하는 자바스크립트 엔진의 메모리는 브라우저의 렌더링 엔진이 동작하는 곳과는 별개의 공간이다. 브라우저는 자바스크립트 엔진과 렌더링 엔진을 분리하여 동작하며, 자바스크립트 엔진은 가상돔을 비롯한 자바스크립트 객체들을 처리하고 렌더링 엔진은 HTML CSS 이미지 등을 처리한다.

가상돔은 자바스크립트의 일반 객체로 이루어져 있으며, 뷰(View)를 업데이트하는 데 사용된다. 일반적으로 브라우저에서 실제로 표시되는 돔은 매우 복잡하고 대규모일 수 있다. 이러한 돔 구조를 변경하면 브라우저가 렌더링하는데 많은 시간이 걸릴 수 있다. 이는 병목현상을 야기할 수도 있으며 많은 조작이 이루어질 경우 성능 저하를 초래할 수 있다. 이러한 문제를 해결하기 위해 리액트와 같은 라이브러리는 가상돔을 사용하여 돔 조작을 최소화하고 성능을 개선한다.

가상돔은 위의 문제를 해결하기 위해 사용된다. 브라우저에서 표시되는 실제 돔과 동기화 되지는 않는다. 대신 업데이트된 가상돔을 사용하여 이전 가상돔과 비교하여 변경된 부분만 실제 돔에 적용한다.

이를 통해 브라우저에서 필요한 최소한의 업데이트만 수행하여 성능을 개선하게된다.

💡 가상돔 역시도 자원을 소비하기에 가상돔의 업데이트 또한 최소화해야한다. 이를 위해 리액트와 같은 라이브러리는 변경되는 부분만 업데이트하고 불필요한 렌더링을 방지하여 성능을 최적화한다.

리액트는 가상돔을 사용함으로써 UI를 빠르게 업데이트 할 수 있도록 도와준다.

이를 통해 개발자는 UI 업데이트 로직을 직접 작성하는 대신에 리액트라는 라이브러리에게 위임하므로 생산성을 높일 수 있다. 또한 가상돔은 리얼돔과는 별개의 메모리 공간에서 작동하므로 UI 업데이트 작업이 빠르고 효율적으로 처리 될 수 있다.

성능향상

UI를 업데이트하는 방법에는 여러가지 방법이 있다.

가장 간단한 방법으로는 UI 전체를 새로 렌더링하는 방법이다. 하지만 이 방법은 UI의 크기가 커질수록 렌더링 시간이 길어지는 문제가 있고 변경되지 않아도 되는 부분까지 모두 리렌더링이 필요하므로 불필요한 작업이 많아진다.

React에서는 가상돔과 Diffing 알고리즘을 사용하여 변경된 부분만 실제 돔에 업데이트 함으로써 성능을 향상시킨다. 가상돔과 리얼돔은 동일한 구조를 갖지만, 메모리 상에 존재하므로 실제 돔보다 빠른속도로 UI업데이트 작업을 처리할 수 있다.

Diffing 알고리즘 Diffing 알고리즘은 이전 가상돔과 새로운 가상돔을 비교하여 변경된 부분을 찾아내는 과정이다. 이전 가상돔과 새로운 가상돔은 각각 하나의 트리 구조로 표현되며, 이 트리 구조를 비교하여 노드 단위로 변경된 부분을 탐색한다.

노드의 타입이나 속성, 자식 노드 등을 비교하여 변경된 부분을 탐색한다. 노드의 타입이나 속성이 변경된 경우 해당 노드를 업데이트하고 자식 노드가 변경된 경우 해당 노드의 자식 노드를 비교하여 변경된 부분을 찾아낸다.

Diffing 알고리즘의 구체적인 동작 방식은 아래와 같다.

  1. 트리 순회 이전 가상돔과 새로운 가상돔을 순회하면서 각 노드의 타입과 속성을 비교한다. 이를 위해 가상돔은 트리구조로 이루어져 있으며 깊이 우선 방식으로 순회한다.
  2. 변경 여부 확인 각 노드의 타입과 속성을 비교한 후 이전 가상돔과 새로운 가상돔이 동일한 노드인지 비교한다. 노드가 동일할 경우 변경되지않았다고 판단한 후 다음 노드로 이동한다.
  3. 노드 추가/제거 동일한 노드가 아닐 경우 해당 노드를 추가하거나 제거한다. 추가하는경우 새로운 노드를 생성하고 제거하는 경우 이전 노드를 제거한다.
  4. 속성 업데이트 노드가 동일하지만 속성이 변경된 경우 속성을 업데이트한다. 이전 속성과 새로운 속성 값을 비교하여 변경된 속성만 업데이트하고 변경되지 않은 속성은 그대로 유지한다.
  5. 자식 노드 순회 자식 노드가 있는 경우 이전 가상돔과 새로운 가상돔의 노드를 순회하며 Diffing 알고리즘을 재귀적으로 적용한다. 이 과정에서 변경된 부분이 있다면 해당 노드를 업데이트하고 변경되지 않은 부분은 그대로 유지한다.

Reconciliation 알고리즘

Reconciliation 알고리즘은 Diffing 알고리즘으로 찾아낸 변경된 부분을 실제 돔에 반영하는 과정이다. 변경된 부분이 많은 경우에는 모든 변경사항을 적용하는 것이 아니라, 변경된 부분만 최소한의 작업으로 처리하여 성능을 향상시킨다.

변경된 부분을 실제 돔에 적용하기 위해 변경된 노드를 다시 그려야 하는 경우와 기존 노드를 업데이트하는 경우로 나눌 수 있다. 기존 노드를 업데이트 하는 경우에는 변경된 속성값이나 자식 노드를 새로운 값으로 갱신한다.

Reconciliation 알고리즘의 구체적인 동작 방식은 아래와 같다.

  1. 노드 추가/제거 Diffing 알고리즘에서 추가되거나 제거된 노드가 있다면, 해당 노드를 리얼돔에 추가하거나 제거한다. 추가되는 경우 리얼돔에 새로운 노드를 생성하고 제거되는 경우 해당 노드를 제거하고 새로운 위치에 새로운 노드를 생성한다.
  2. 노드이동 노드의 위치가 변경된 경우, 해당 노드를 새로운 위치에 추가한다. 이를 위해 리얼돔에서 해당 노드를 제거하고 새로운 위치에 새로운 노드를 생성한다
  3. 속성업데이트 Diffing 알고리즘에서 속성이 변경된 노드가 있다면 해당 노드의 속성을 업데이트한다. 이를 위해 리얼돔에서 해당 노드의 속성을 업데이트한다.
  4. 자식노드 순회 자식 노드가 있는 경우 이전 가상돔과 새로운 가상돔의 자식 노드를 순회하면서 Reconciliation 알고리즘을 재귀적으로 적용한다. 이 과정에서 변경된 부분이 있다면 해당 노드를 업데이트하고 변경되지 않은 부분은 그대로 유지한다.

메모이제이션에 대한 나의 생각을 적어주세요.

Dan Abramov와 Coinbase 개발자 Stefano J. Attardi가 메모이제이션을 두고 벌인 논쟁을 보면서, 내가 그동안 가지고 있던 생각들이 정리되는 느낌이었다.

Stefano의 주장을 보면 현실적인 면이 강하다. 대규모 팀에서 모든 개발자가 언제 메모이제이션을 써야 하는지 완벽하게 판단할 수 있을까? 사실 어렵다. 그래서 아예 모든 곳에 메모이제이션을 적용하자는 게 그의 접근법이다. 마스크 착용 의무화와 비슷한 논리인데, 개별 판단에 맡기는 것보다 일괄적으로 적용하는 게 더 안전하다는 것이다.

실제로 메모이제이션을 깜빡하면 렌더 함수 호출, 콜백 재생성, JSX 할당 등이 연쇄적으로 발생하면서 자식 컴포넌트까지 영향을 미친다. 게다가 리액트가 이미 가상돔으로 이전 렌더 결과를 보관하고 있어서 메모리 오버헤드도 크지 않다는 점도 설득력이 있다.

반면 Dan Abramov는 완전히 다른 철학을 가지고 있다. 메모이제이션은 근본적인 해결책이 아니라 최후의 수단이라는 입장이다. 컴포넌트 구조를 제대로 설계하면 메모이제이션 없이도 성능 문제가 해결된다고 본다.

그가 제시한 방법들을 보면:

  • 상태를 필요한 최소 범위로 이동시키기 (Move State Down)
  • children prop을 활용해서 변경되지 않는 부분 분리하기 (Lift Content Up)

이런 접근법들이 실제로 효과적이라는 건 경험해본 적이 있다.

사실 나도 예전부터 "메모이제이션이 잘못 설계된 컴포넌트 구조를 땜질하는 용도가 아닐까?" 하는 의구심을 가져왔다. 이번 논쟁을 보면서 그 생각이 어느 정도 맞다는 확신이 들었다.

특히 두 가지 상황에서 그런 느낌이 강하다:

첫째, 상태 배치가 잘못된 경우다. 상태를 불필요하게 상위 컴포넌트에 두고 나서 메모이제이션으로 성능 문제를 해결하려고 하는 경우가 많다. 하지만 상태를 적절한 위치에 배치하면 애초에 문제가 발생하지 않는다.

둘째, 컴포넌트 책임 분리가 제대로 되지 않은 경우다. 하나의 컴포넌트가 너무 많은 역할을 담당할 때 불필요한 리렌더링이 발생하는데, 이때도 메모이제이션보다는 단일 책임 원칙에 따라 컴포넌트를 분리하는 게 더 근본적인 해결책이다.

그렇다고 해서 메모이제이션이 전혀 필요 없다는 건 아니다. 현실적으로 메모이제이션이 필요한 상황들이 분명 존재한다.

레거시 코드베이스의 경우, 이미 복잡하게 얽힌 구조를 단번에 리팩터링하기는 현실적으로 어렵다. 이런 상황에서는 점진적인 개선 과정에서 메모이제이션이 부패 방지 계층 역할을 할 수 있다.

또한 대규모 팀에서는 Stefano의 지적처럼 모든 개발자가 완벽한 구조 설계를 하기 어려운 것도 사실이다. 이럴 때는 메모이제이션을 안전장치로 활용하는 것도 하나의 방법이다.

결국 내 생각은 메모이제이션 자체가 나쁜 도구는 아니지만, 사용하기 전에 "왜 이 컴포넌트에서 불필요한 리렌더링이 발생하는가?"라는 질문을 먼저 던져봐야 한다는 것이다.

Dan Abramov가 말한 것처럼 올바른 컴포넌트 구조는 단순히 성능 개선뿐만 아니라 데이터 플로우를 명확하게 만들고, props drilling을 줄이며, 전체적인 코드 품질을 향상시킨다. 이런 측면에서 구조적 개선이 우선되어야 한다고 본다.

참고자료


컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.

Context는 상태관리 도구라기보다는 컴포넌트의 관심사 범위를 제한해주는 격리 레이어에 가깝다고 생각한다. Redux의 메인테이너인 Mark Erikson도 Context를 Dependency Injection Tool이라고 표현하는데, 이 관점이 훨씬 정확한 것 같다.

실제로 진정한 상태관리 도구라면 4가지 요구사항을 만족해야 한다:

  1. 초기값 저장
  2. 현재값 읽기
  3. 값 업데이트
  4. 변경 알림

그런데 React Context는 이 중에서 값 업데이트 기능이 빠져있다. Context의 값을 바꾸려면 결국 외부 시스템(useState, useReducer 등)에 의존해야 한다.

흥미로운 건 유명한 상태관리 라이브러리들을 보면 모두 Context를 의존성 주입 용도로 사용한다는 점이다:

  • Redux는 Store 인스턴스와 Subscription 객체를 Context로 전달한다
  • React Query는 QueryClient 객체를 Context로 넘긴다
  • Apollo는 Client 객체를, MobX는 Observable 객체를 Context로 주입한다

이들은 모두 Context를 통해 관찰 가능한 객체나 구독 가능한 컨테이너를 컴포넌트 트리에 주입시키고, 실제 상태관리는 각 라이브러리 자체에서 처리한다.

이런 격리 레이어가 주는 핵심 가치는 무엇일까?

우선 테스트가 훨씬 쉬워진다. 네트워크 요청을 모킹하지 않고도 간단하게 테스트할 수 있다:

test('loads products', async () => {
  const mockFactory = () => ({
    async lookupAllProducts() {
      return [{id: 1, title: 'Test Product'}];
    }
  });
  
  render(
    <DepsProvider productServicesFactory={mockFactory}>
      <Products />
    </DepsProvider>
  );
  
  // 테스트 로직
});

또한 관심사 분리가 자연스럽게 이뤄진다. Presentation layer는 렌더링과 사용자 상호작용에만 집중하고, Business layer는 복잡한 비즈니스 로직을 처리하며, Data layer는 HTTP나 GraphQL, 캐싱 같은 구현 세부사항을 숨길 수 있다.

이걸 보면서 클린 아키텍처의 저자 Robert Cecil Martin이 한 말이 떠올랐다:

"패러다임은 개발자의 권한을 박탈한다"

  • 구조적 프로그래밍은 goto문 사용 권한을 박탈했고
  • 객체지향 프로그래밍은 함수 포인터의 직접 사용을 막았으며
  • 함수형 프로그래밍은 할당문 사용을 제한했다

이런 패러다임들은 모두 개발자가 할 수 있는 일을 제한함으로써 더 나은 소프트웨어를 만들도록 유도한다.

Context도 마찬가지다. 개발자의 여러 권한을 박탈한다:

  1. 전역 모듈을 import해서 컴포넌트 내에서 직접 사용할 권한
  2. 데이터를 원하는 대로 props로 전달할 권한
  3. Presentation 레이어와 Business 레이어를 한 곳에서 처리할 권한

디자인 패턴도 비슷하다. 자유를 박탈해서 질서를 만든다:

  • Observer 패턴은 객체 간 직접 통신을 막고 중재자를 통해서만 소통하게 한다
  • Factory 패턴은 생성자 직접 호출을 금지하고 팩토리를 통해서만 객체를 만들게 한다
  • Dependency Injection은 의존성을 직접 생성하지 못하게 하고 외부에서 주입받도록 강제한다

처음에는 이런 제약들이 불편해 보이지만, 결과적으로는 예측 가능한 코드 구조를 만들고, 테스트 용이성을 높이며, 유지보수성을 개선하고, 팀 협업 효율성을 증대시킨다.

Context는 이런 문제들을 해결하기 위한 수단이라고 생각한다. 단순히 전역 상태 저장소로 보는 관점에서 벗어나서, 개발자의 자유를 의도적으로 제한함으로써 더 나은 소프트웨어 구조를 강제하는 아키텍처 도구로 봐야 하지 않을까 싶다.

참고자료

과제 피드백

안녕하세요 준형님! 역시 믿고보는 준형님의 과제입니다 ㅎㅎ

이러한 학습 과정에서 Robert Cecil Martin의 "패러다임은 개발자의 권한을 박탈한다"는 말이 깊이 와닿았습니다. 좋은 코드란 개발자의 자유를 의도적으로 제한함으로써 예측 가능하고 함께 작성하기 좋은 구조를 강제하는 코드라는 철학을 갖게 되었습니다.

자유를 박탈한다는 표현이 무척 와닿네요 ㅋㅋ 사실 프레임워크 기반의 코드가 그런 편이죠.

개인적인 소견으로는 답이 없는 과제들이 가장 어렵고 힘들었지만 제일 인상적이었습니다. 정해진 정답을 찾아가는 과정이 아니라, 나의 생각들을 직접 정리해가면서 구체화시켜가는 과정이었기 때문입니다. 이러한 과정을 통해 단순히 기술적 구현 능력뿐만 아니라, 개발자로서의 사고 체계와 철학을 정립할 수 있는 귀중한 기회가 되었습니다.

오호.. 그렇군요! 의도한 부분은 아니었어요 ㅎㅎ 사실 저는 답이 있는걸 별로 좋아하진 않는 편이긴 해요. 다만... 과정의 특성상 어느정도의 솔루션이 필요하다보니 밸런스를 맞추는 과정에서 이러한 모습이 되었네요.

준형님이 좋은 인사이트를 주셔서, 다음 기수는 조금 더 어렵게(?) 만들어갈 수 있을 것 같아요 ㅋㅋ 감사합니다!

결국 내 생각은 메모이제이션 자체가 나쁜 도구는 아니지만, 사용하기 전에 "왜 이 컴포넌트에서 불필요한 리렌더링이 발생하는가?"라는 질문을 먼저 던져봐야 한다는 것이다. Dan Abramov가 말한 것처럼 올바른 컴포넌트 구조는 단순히 성능 개선뿐만 아니라 데이터 플로우를 명확하게 만들고, props drilling을 줄이며, 전체적인 코드 품질을 향상시킨다. 이런 측면에서 구조적 개선이 우선되어야 한다고 본다.

저도 동의합니다! 이런 훈련이 계속 되어야 잘 사용할 수 있는 것 같아요.


이 외에는 특별한 질문이 없는 것 같아서 피드백은 여기서 마무리하겠습니다 ㅎㅎ 너무 고생 많으셨어요 준형님!!