과제 체크포인트
배포 링크
https://dev4n4.github.io/front_6th_chapter1-3/
기본과제
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 개선
과제 셀프회고
React의 Hook들에 대해 deepdive 해보는 계기가 되어서 좋았다. Hook들은 당연히 JS로 구현이 되어 있었겠지만… Hook을 직접! JS로 작성해 볼 수도 있다는 생각은 안해봤는데 이렇게 과제로 제시받아 직접 해보니까 이해도가 올라가고 Hook의 내부 구조에 대해 고민해보고 알 수 있게 되어서 정말 좋았다..
나는 기초가 부족한 편이었다고 스스로 생각하고 있었기도 해서 이번 기회에 보완하는 데 많은 도움을 받았던 것 같다.
기술적 성장
👍 새로 학습한 개념
Object.is
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is
이번 과제에서 많이 사용했던 메서드이고 == 연산자, === 연산자와 어떤 점이 다른지에 대해 잘 아는 계기가 되었던 것 같다.
- == 연산자와의 차이점
==연산자는 같음을 테스트하기 전에 양 쪽(이 같은 형이 아니라면)에 다양한 강제(coercion)를 적용하지만("" == false가true가 되는 것과 같은 행동을 초래),Object.is는 어느 값도 강제하지 않습니다.
- === 연산자와의 차이점
Object.prototype.hasOwnProperty() 대신 Object.hasOwn() 사용하기
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn
지금까지 Object.prototype.hasOwnProperty()를 사용해 왔었는데, hasOwn을 사용하는게 권장된다는 걸 이번에 첨 알았다!
Object.hasOwn() 가 권장됩니다. hasOwnProperty() 는 이를 지원하는 브라우저에서만 사용됩니다.
💪 기존 지식의 재발견/심화
useRef
useState를 활용해 useRef를 만들 수 있다는 것을 이번에 알았다!
두 Hook에 대해 다 알고는 있었는데… 이렇게 구현될 수 있다는 것이 신기했고 생각해보면 그렇지 싶은데 왜 지금까지는 별 생각이 없었을까… 사실 잘 몰랐던 게 아닐까 싶었다.
const [value] = useState(...)로 Setter 함수를 무시하면, 해당 객체를 변경해도 리렌더링이 발생하지 않는다.
- 일반적으로
useState를 사용하면 상태를 업데이트할 때setState를 호출해 리렌더링을 유발하지만, 여기서는 setter를 전혀 사용하지 않기 때문에.current만 변경해도 화면 갱신이 일어나지 않는다. 즉,value.current = 새로운값처럼 직접 할당해도 React는 이 변경을 인지하지 않으므로 리렌더링을 하지 않는다.
위 방식은 공식 useRef 훅의 동작 원리와 같다.
useRef는 한 번만 생성된 객체를 기억해 두었다가, 렌더링마다 동일한 객체를 반환한다. 그 객체의.current프로퍼티를 바꿔도 React가 다시 렌더링하지 않는다.useState의 “초기 상태 유지” 특성을 활용해 저장소(ref)를 만든 것이다.
useMemo
useMemo가 이렇게 의존성들을 직접 비교 하면서 메모이제이션을 해주는 것이 신기했다. (생각해보면 비교가 당연히 들어갈텐데 그동안은 왜 이렇게 마법같이 막연하게 느껴졌었는지..?)
export function useMemo<T>(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T {
// 직접 작성한 useRef를 통해서 만들어보세요.
// 1. 이전 의존성과 결과를 저장할 ref 생성
const ref = useRef<{
deps?: DependencyList;
result?: T;
}>({});
// 2. 현재 의존성과 이전 의존성 비교
// 3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장
if (!ref.current.deps || !_equals(ref.current.deps, _deps)) {
ref.current.deps = _deps;
ref.current.result = factory();
}
// 4. 메모이제이션된 값 반환
return ref.current.result as T;
}
자랑하고 싶은 코드
대체로 정답이 있는 코드들 같아서 자랑하고 싶은 코드가 크게 생각나지는 않는다.
그래도 굳이 자랑할 코드를 생각해보자면 equal 함수들을 AI를 사용하지 않고 직접 생각하면서 구현했다는거..? 리팩토링도 한번 했다는거!?
// shallowEquals 함수는 두 값의 얕은 비교를 수행합니다.
export const shallowEquals = (objA: unknown, objB: unknown) => {
// 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우)
if (Object.is(objA, objB)) return true;
// 2. 둘 중 하나라도 객체가 아닌 경우 처리
if (objA === null || objB === null) return false;
if (Array.isArray(objA) && Array.isArray(objB)) {
if (objA.length !== objB.length) return false;
for (let i = 0; i < objA.length; i++) {
if (!Object.is(objA[i], objB[i])) return false;
}
return true;
}
if (typeof objA === "object" && typeof objB === "object") {
// 3. 객체의 키 개수가 다른 경우 처리
const objAKeys = Object.keys(objA);
const objBKeys = Object.keys(objB);
if (objAKeys.length !== objBKeys.length) return false;
// 4. 모든 키에 대해 얕은 비교 수행
for (const key of objAKeys) {
if (!Object.is((objA as Record<string, unknown>)[key], (objB as Record<string, unknown>)[key])) return false;
}
return true;
}
// 이 부분을 적절히 수정하세요.
return false;
};
개선이 필요하다고 생각하는 코드
으음… 이것도 대체로 정답이 있는 코드를 정답의 원인을 찾아가면서 적어나갔던 거라 크게 생각이 나지 않는다.
그래도 전반적으로 자잘하게 (변수명이라던가) 리팩토링을 해서 가독성을 좋게 만들면 더 좋지 않을까 하는 생각이 든다.
학습 효과 분석
React의 Hook들에 대해 deepdive 해 볼 수 있었던 시간이었던 것 같아서 좋았다.
공식 문서를 정독하고, Hook들을 해부해 보면서 막연하게만 생각했던 구조가 현실적으로 다가와 이해도가 깊어졌던 것 같다.
이전에는 뭐랄까 JS와 React를 따로 생각했었는데 지금은 React가 JS 코드로 어떻게 만들어졌는지를 보니까 기초가 탄탄해서 라이브러리에 구애받지 않고 개발을 잘 하는 것에 대해서도 생각을 해보게 되는 것 같고…
기초가 중요하다, JS에 대해서 깊게 이해하는 것이 중요하다 라고 말로만 듣고 실제로 체감해본 적은 없는데 이번 과제들을 수행하며 왜 기초를 탄탄히 해야하는지 직접 체감하고(다 JS로 이루어져있구나 하고 실감이 가서), 앞으로 어떻게 공부를 해야 할 지에 대해서도 감을 잡을 수 있었던 것 같다.
나중에 React나 기타 라이브러리들의 repo를 뜯어보고 싶다는 생각도 들었다!
과제 피드백
기초 과제가 너무 좋았어요! Hook에 대해서 깊은 이해를 할 수 있어서 정말 유익하고 진행하면서 배운 것도 많았어요. 이전 주차들과 연계되는 느낌이 들어서 이해도 빨랐어요!!
심화 과제는 조금 어려웠어요.. AI의 도움을 받으면서 진행을 했고 잘 해결되긴 했으나 아직도 약간 헷갈리는 것 같아요.
Toast에서 좀 헤맸어요! “Context를 분리해야겠다!” 까지는 생각이 금방 도달했는데 useCallback을 쓸 지, useAutoCallback을 쓸 지, useMemo를 쓸 지 고민하고 이것저것 삽질을 했던 것 같아요.
근데 삽질하면서 이전에 구현했던 부분들을 다시 읽어보면서 복습도 되고 좋았어요!
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리액트의 랜더링 과정
- 리액트는 상태(state)나 props가 변경되면 렌더링이 발생합니다.
- 변경이 감지되면 Virtual DOM에서 새로운 트리(가상 노드)를 생성하고, 이전 Virtual DOM과 비교하는 Reconciliation 과정을 거칩니다.
- 이때 실제 DOM에는 변경된 부분만 최소한으로 반영하여 브라우저의 성능을 최적화합니다.
리액트의 렌더링 최적화 방법
React.memo,useMemo,useCallback을 활용해 불필요한 리렌더링을 방지할 수 있습니다.- 상태를 분리하거나, 컴포넌트를 나눠서 렌더링 범위를 줄이는 방식도 많이 사용됩니다.
key값을 적절히 부여하거나shouldComponentUpdate,PureComponent를 사용하는 것도 최적화 방법 중 하나입니다.
리액트의 렌더링과 관련된 개념
- Virtual DOM: 실제 DOM보다 가벼운 JavaScript 객체로, 변경 사항을 빠르게 계산할 수 있도록 도와줍니다.
- Reconciliation: 이전 Virtual DOM과 새 Virtual DOM을 비교(diffing)하여 최소한의 변경만 실제 DOM에 반영하는 과정입니다.
- Batching: 여러 상태 업데이트를 하나로 묶어 한 번에 처리함으로써 렌더링 횟수를 줄이는 기법입니다.
렌더링과 관련된 라이프사이클 & Hook
- 클래스 컴포넌트:
componentDidMount,shouldComponentUpdate,componentDidUpdate등 - 함수형 컴포넌트:
useEffect,useMemo,useCallback,useLayoutEffect,useSyncExternalStore등
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션
- 메모이제이션은 계산 비용이 높은 연산 결과를 재사용하기 위해 사용합니다.
useMemo,useCallback,React.memo등으로 값이나 함수의 재생성을 방지할 수 있습니다.
메모이제이션이 언제 필요할까?
- 렌더링 시 무거운 계산이 자주 발생하는 경우
- 동일한 props로 자식 컴포넌트가 자주 리렌더링 되는 경우
- 콜백 함수가 자식 컴포넌트에 props로 전달될 때, 참조가 매번 바뀌는 걸 방지하고 싶을 때
메모이제이션을 사용하지 않으면?
- 렌더링이 자주 발생하고, 불필요한 연산으로 인해 성능 저하가 생길 수 있습니다.
장점
- 불필요한 연산/렌더링을 줄여 성능을 높일 수 있음
- 렌더링 결과의 일관성을 유지할 수 있음
단점
- 코드 복잡도가 증가함
- 의존성 배열 관리가 까다로울 수 있음
- 너무 남용하면 오히려 성능이 저하될 수 있음
메모이제이션을 사용하지 않고도 해결할 수 있는 방법
- 렌더링 자체를 줄일 수 있는 구조로 컴포넌트를 쪼개거나 상태를 최적화하는 방향으로 설계하기
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
컨텍스트와 상태관리가 필요한 이유
- 전역적으로 공유되는 값(예: 사용자 정보, 테마, 언어 등)을 여러 컴포넌트에서 편리하게 사용할 수 있게 해줍니다.
- 깊은 컴포넌트 트리를 props drilling 없이 상태를 전달할 수 있게 해줍니다.
컨텍스트와 상태관리를 사용하지 않으면?
- props를 계속 전달하게 되면서 코드가 지저분해지고 유지보수가 어려워짐
- 중첩된 컴포넌트에서 상태 접근이 비효율적임
장점
- 전역 상태 공유가 쉬워짐
- 특정 도메인 상태를 분리하여 관리할 수 있음 (예: 로그인, UI, 토스트 등)
단점
- 남용하면 모든 컴포넌트가 렌더링되며 성능 저하 발생
- 컨텍스트가 바뀔 때마다 해당 컨텍스트를 구독 중인 모든 컴포넌트가 리렌더링됨
컨텍스트와 상태관리를 사용하지 않고도 해결할 수 있는 방법
- 컴포넌트 단위에서 상태를 관리하고, 필요한 경우에만 컨텍스트 사용하기
- 전역 상태 관리 도구(Zustand, Redux 등)를 도입해 더 정교한 상태 관리 가능
사용할 때 주의할 점
- 실제로 전역으로 공유할 필요가 있는 데이터만 컨텍스트로 관리하기
- 값 변경이 자주 발생하는 상태는 컨텍스트보다는 로컬 상태나 store 기반 접근이 더 유리함
리뷰 받고 싶은 내용
-
useMemo, useCallback, useAutoCallback을 구현할 때 마지막으로 값을 리턴할 때 타입 단언을 사용하여 “as T” 형식으로 리턴하였는데 이보다 나은 방법이 있을 지 궁금합니다. 약간 “any” 처럼 TS의 검사를 소홀히 하는 방식으로 빠져나간게 아닌가 싶어서요. 근데 보기에는 깔끔해 보이니 괜찮은가 싶기도 한데 TS의 의도대로 사용하지 못한 것 같기도 해서 자주 쓰면 안좋은 문법인가 하는 생각이 들기도 합니다. 코치님께서는 타입 단언을 사용하여 구현하는 것에 대해 어떻게 생각하시나요? 해당 케이스들에서는 어떻게 구현하는 것을 추천하시나요?
-
ToastProvider 구현에 관한 질문입니다. 지금은 Context를 나누고, useMemo를 사용해서 최적화를 진행하였습니다. 그런데 이게 최선일지… 의도하신 대로 제가 문제를 잘 푼건지 궁금합니다. 뭔가 비슷한 형태의 코드가 반복되는 것도 같은데 반복되는 부분이 Context 선언하는 부분이니까 어쩔수 없나 싶기도 하면서도 여기서 조금 더 코드를 개선할 수도 있지 않을까 생각이 들기도 하네요. 여기서 조금 더 깔끔하고 예쁘게 코드를 고치려면 어떻게 나아가야 할까요?
과제 피드백
안녕하세요 산들! 수고 많으셨습니다. 이번 과제는 React의 내장 훅들을 직접 구현해보면서 프레임워크가 어떻게 상태를 관리하고 최적화하는지 이론을 넘어 몸으로 깊이 이해하는 것이 목표였습니다.
몇몇 함수들은 AI를 사용하지 않고 직접 고민하며 구현해본 과정 칭찬합니다. 스스로 도전해서 내가 이정도까지 할 수 있다는 확신을 가지는 경험을 쌓아가는건 개발자로써 정말 중요한 경험이죠. 이렇게 세부 구현을 하게 되면서 React라는 도구가 흑마법이 아니라 React도 그저 JavaScript로 구현된 논리적인 시스템이라는 것을 체감하면서 원리가 이해가 되었을거라 생각해요.
Context를 이용한 개념도 이번 기회에 잘 이해하게 된것 같아서 좋네요. 상태관리 라이브러리들도 이렇게 상태와 액션을 분리해서 다루고 있죠. 이런 개념적 분리가 아키텍처를 더 선명하게 바라볼 수 있게 해줄거에요.
Q) 타입 단언(as T) 사용에 대해
=> 가급적 사용하지 않도록 하는 것이 제일 좋지만 타입스크립트에서 이러한 문법을 만들었다는 건 다 필요한 부분은 있다라는 것이죠. 사람마다 취향은 다른데 저는 최대한 타입 추론을 통해서 가능하도록 만들고자 하는 편입니다.
그렇지만 사용자에게 제공하는 라이브러리를 만들 때에는 내부의 복잡한 구조로 인해서 타입이 굉장히 복잡해지는 경우가 있는데 이럴때 타입 단업을 통해서 사용자에게 전달해지는 라이브러리는 깔끔하게 정리할수가 있죠.
Proxy라던가 사실 꼭 T 타입이 아닐수도 있는 타입들을 단언을 통해서 해당 타입을 강제할수도 있구요.
가급적 타입추론을 통해서 해결하고자 하되 내가 의도가 있어서 as T라고 써야 할때 쓰는 건 좋다고 생각합니다. 반면 가장 추상화 바깥의 영역에서 단지 type 에러를 수정하기 위해서 사용하는건 지양해야 곘죠.
Q) ToastProvider 개선 방향
=> 현재 구현도 충분히 깔끔합니다! Context 분리와 useMemo 활용이 잘 되어있어요. 더 개선한다면 로직을 Custom Hook으로 추출해볼 수 있겠네요. 예를 들어 useToastLogic 같은 훅을 만들어 debounce 로직과 상태 관리를 캡슐화하면 Provider 컴포넌트가 더 간결해질 거예요. 하지만 현재 수준에서도 충분히 실무에서 사용할 만한 품질입니다.
이번 과제를 통해 "기초가 중요하다"는 말의 진정한 의미를 체감하셨다니 정말 기쁩니다. useState로 useRef를 구현할 수 있다는 발견, useMemo가 의존성을 직접 비교한다는 깨달음 - 이런 "아하!" 모먼트들이 쌓여서 깊이 있는 개발자로 성장하게 됩니다.
React 레포지토리를 뜯어보고 싶다는 열망도 멋집니다! 그런 호기심이 계속된다면 머지않아 오픈소스에 기여하는 개발자가 되실 수도 있을 거예요.
수고하셨습니다. 클린코드 챕터에서도 이런 깊이 있는 탐구를 계속 이어가시길 바랍니다. 화이팅입니다! :)