과제 체크포인트
배포 링크
https://annkimm.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 개선
과제 셀프회고
기존에 너무 AI에 의존한 것에 대해서 어떻게 하면 개선할 수 있을까 고민하다가 대신에 AI에게 코드 없이 구현해야하는 함수를 어떻게 하면 짤 수 있을지 순서를 정하고 흐름을 알려주는 방식으로 바꿨다. 이른바 AI를 튜터처럼 쓰기 개시...!
가끔 너무 많은 흐름을 알려줄 때도 있었지만, 두번째 과제보다는 이번 과제를 해보면서 과제를 스스로 구현할 수 있는 힘을 좀 더 기르고 있다는 생각이 들었다.
기술적 성장
-
다른 훅들도 의외로 useState와 useRef를 기반으로 구현 이를 통해 useState와 useRef가 훅의 근간을 이루는 필수 개념임을 깨달았다.
-
useAutoCallback과 useCallback의 차이점에 대한 이해 그동안 useCallback을 너무 무지성으로 썼다는 것을 알 수 있었던 지점 중에 하나였다. useCallback은 의존성 배열에 따라 값이 바뀌면 새로 참조된다는 것을 새롭게 알 수 있었고, useAutoCallback은 한번만 참조하기 때문에 특히나 성능 최적화에 유리하다는 것까지 이번에 알 수 있었다.
-
useShallowSelector와 useShallowState 차이 useShallowSelector -> 얕은 비교를 하는 함수의 역할, (useSyncExternalStore 같은 store가 없기 때문에 구독을 할 수 없다) useShallowState -> context api, useState 같은 데에서 불필요한 리렌더링을 줄일 때 사용하는 역할
-
useStorage, useStore 차이 useStorage -> localStorage 같은 Storage에서 값이 변경되면 컴포넌트를 재리렌더링 역할 useStore -> 구독해서 바뀌는 것, 즉 zustand같은 라이브러리 역할
-
생각보다 deepEqual은 사용하지 않는다는 점을 봤을 때, 연산에 대한 성능을 위해 쓰지 않는걸까라는 생각이 들었다.
자랑하고 싶은 코드
없다...
개선이 필요하다고 생각하는 코드
shallowEquals에서 객체로 비교한 구문과 배열로 비교한 구문을 따로 함수로 뺐으면 좋았을 것 같다.
학습 효과 분석
기술적 성장으로 대체
과제 피드백
저는 2주차 과제보다 이번주차 과제가 더 재밌었어요... 회사에서 아무 생각없이 훅을 쓰게 되는데 여기서 구현해봄으로써 이건 굳이 필요할까? 라는 생각을 더 해보게 되었습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
- 업데이트 트리거
- 사용자 상호작용 (클릭, 입력 등)
- setState() 또는 useState setter 호출
- useReducer의 dispatch() 실행
- 부모로부터 받은 props 변경
- 네트워크 응답 또는 타이머 콜백
- 스케줄링
- 들어온 업데이트를 배치 처리
- 동기 모드(이벤트 루프 후 즉시)
- Concurrent 모드(우선순위에 따라 분할 실행)
- React 스케줄러로 작업 정리
- 렌더(Render) 단계
- 함수형 컴포넌트 실행 또는 클래스 render() 호출
- 새로운 가상 DOM 트리 생성
- 순수 연산만 수행 (부작용 없음)
- 리콘실리에이션(Reconciliation)
- 이전 가상 DOM과 새 가상 DOM 비교(diff)
- 변경될 노드 삽입·삭제·속성 업데이트 목록 계산
- key 기반으로 리스트 항목 효율적 매핑
- 커밋(Commit) 단계
- Before Mutation: getSnapshotBeforeUpdate 호출
- Mutation: 실제 DOM에 노드 추가·삭제·속성 변경
- Layout: useLayoutEffect 또는 componentDidUpdate 동기 실행
- 사이드 이펙트 처리
- 브라우저 레이아웃 완료 후 실행
- useEffect 또는 componentDidMount/componentDidUpdate
- 네트워크 요청, 구독 설정, 이벤트 등록 등 수행
메모이제이션에 대한 나의 생각을 적어주세요.
- 기본적으로 리액트가 성능 최적화를 다해주기 때문에 굳이 메모리제이션을 쓸 필요는 없다고 생각합니다.
- 다만 굳이 사용한다면 API 통신으로 값을 업데이트할 때나 같은 함수나 연산이 반복된다면 그 정도는 할만하다고 생각합니다.
- 메모리제이션으로 캐시를 물고 있는 것 자체도 다 성능에 문제를 일으킬 수 있기 때문에 굳이 꼭 필요한 게 아니라면 저는 사용하는 것 보단 사용하지 않는 걸 더 추천합니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
- Context API를 사용해보면서의 느낀 장점은 일단 이전에는 컴포넌트에 모든 값을 props로 전달해줘야 해서 props drilling이 너무 깊어져서 일일히 props 전달하는 것도 굉장히 귀찮은 일이고, 어디까지 이어져 있는지 알 수 없어서 힘든 일이엇지만, 이제는 Context API로 한 번 선언하면 어디서든 간단하게 가져다 쓸 수 있는게 굉장히 편리하고 간단해서 메리트라고 생각이 들었습니다.
- 하지만 하나의 페이지 안에 데이터가 굉장히 많아지게 되면 Context API에 그 데이터를 다 관리하다보니 관리하기도 힘들고 너무 불필요한 렌더링이 여러번 일어난다는 걸 알 수 있었습니다. 이번에 토스트 과제에서
ToastContext부분을 함수 부분과 변수부분으로 나눠서 관리하는 방법을 보고 아 저런 방법이 있겠구나 생각이 들었습니다. 오...너무 무거워지지 않게 분산 투자는 중요하구나..(유레카...?!)
리뷰 받고 싶은 내용
-
이번에 useCallback와 useAutoCallback에 대해서 구현하면서 생각이 든건데 useCallback은 의존성 배열에서 따라서 새로 참조한다고 이번에 알게 되었는데, 그냥 드는 생각은 의존성 배열 대신에 매개변수로 받아서 useAutoCallback으로 하먄 될 꺼 같은데 왜 useCallback가 생겨났는지에 대한 이해가 잘 되지 않더라구요. 이렇게 함으로써 장점이 있나요? 성능 최적화의 부분으로써는 최악? 이라는 생각이 드는데.. 의존성 배열에 값이 바뀌면 다시 계속 새로 참조해야 하잖아요.
-
deepEquals에서 배열 부분을 짤 때 flat을 이용해서 평탄화시킨 다음에
JSON.stringify를 사용해서 짰는데요.. 역시 이렇게 짜면 안좋은걸까요? for문이나 이런 걸로 하나씩 다 비교하는 게 맞는 걸까요? (과제는 다 통과하긴 했지만...) 뭔가 더 간단한 방법이 없을까 싶어서 해보긴 했지만 틀린건가라는 생각이 들더라구요.
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
const flattedA = a.flat(Infinity);
const flattedB = b.flat(Infinity);
return JSON.stringify(flattedA) === JSON.stringify(flattedB);
}
과제 피드백
안녕하세요 민지! 이번 1-3 과제 정말 수고하셨습니다. AI 의존도를 줄이고 "AI를 튜터처럼 쓰기"라는 방식으로 과제를 진행했군요! 아주 잘했습니다. 코드를 만들어가는 시행착오를 겪어가며 이해하기를 원했는데 이렇게 직접 구현해보신 과정에서 분명 많은 것을 체득하셨을 거예요.
이번 과제는 React의 내장 훅들을 직접 구현해보면서 프레임워크가 어떻게 상태를 관리하고 최적화하는지 깊이 이해하는 것이 목표였습니다. useState와 useRef가 훅의 근간을 이룬다는 깨달음, 그리고 Context를 상태와 액션으로 분리하여 불필요한 리렌더링을 방지하는 패턴을 경험해보신 점이 특히 좋았습니다. 이런 구조적 사고는 실무에서도 정말 중요한 인사이트예요.
shallowEquals에서 Object.keys를 sort는 비용이 비쌉니다. 직접 키를 순회하며 비교하는 것이 성능상 더 유리해요. deepEquals에서 JSON.stringify를 사용하신 아이디어도 좋지만 이것도 비싼 동작입니다. 운서가 알려준 재귀적 비교가 성능면에서 더 낫습니다. 이러한 시행착오 또한 경험으로 알고나야 정말 내것이 되므로 너무 좋은 시도였다라고 생각해요!
Q) 이번에 useCallback와 useAutoCallback에 대해서 구현하면서 생각이 든건데 useCallback은 의존성 배열에서 따라서 새로 참조한다고 이번에 알게 되었는데, 그냥 드는 생각은 의존성 배열 대신에 매개변수로 받아서 useAutoCallback으로 하먄 될 꺼 같은데 왜 useCallback가 생겨났는지에 대한 이해가 잘 되지 않더라구요. 이렇게 함으로써 장점이 있나요? 성능 최적화의 부분으로써는 최악? 이라는 생각이 드는데.. 의존성 배열에 값이 바뀌면 다시 계속 새로 참조해야 하잖아요.
=> 우선 useCallback은 공식적으로 React에서 제공하는 것이고 useAutoCallback은 우리가 한번 만들어본 Custom Hook입니다.
React는 컴포넌트가 리렌더를 하면 컴포넌트 내부의 모든 useState와 계산 그리고 handler를 새로 만들게 되어 있는데, React에서 최적화라는 건 새로 만들지 않아도 되는 경우에는 기존에 있는 것을 활용하는 방식이죠.
그래서 useCallback와 useMemo등에 의존성 배열을 명시해서 이 값들이 변하지 않으면 새로 만들지 않아도 되니 기존의 값을 재사용해서 최적화 해! 라는 식으로 최적화가 이뤄지게 됩니다.
=> 함수를 새로 생성하는 건 useMemo와 달리 복잡한 계산이 필요없으므로 딱히 비싼 동작이 아닙니다. 사실 useCallback가 더 큰 의미가 생기는 건 이 함수를 자식컴포넌트로 넘길때이죠. 대개 컴포넌트는 props가 변하면 리렌더링이 발생하는데 매번 새로 생성을 하면 최적화가 되지 않겠죠. 그래서 useCallback을 사용하면 기존 값을 그대로 쓸수가 있고, 이를 통해 컴포넌트 최적화가 이뤄집니다.
=> useCallback의 자체적인 최적화 비용은 비싸지 않고 의존성 배열을 입력하는건 귀찮은데다가 실수할 여지도 많으니 항상 새롭게 생성은 하되 기존의 참조값는 전달하도록 해서 자식 컴포넌트에 props로 전달해도 최적화를 할 수 있도록 만든데 useAutoCallback입니다. 이건 공식적인 방식이라기 보다는 개발자들이 생각해보면 일종의 편의함수에요. useCallback보다 덜 엄격한 최적화를 하는대신 편리를 택하는 거죠.
나중에 스스로 다른 사람에게 이걸 강의한다 생각하고 설명하듯이 말해보는 연습을 해보세요. 그러면 왜 그런지 이해하는데 더 도움이 될거에요.
Q) deepEquals에서 flat과 JSON.stringify 사용에 대해
충분히 생각해볼 수 있는 접근이었다 생각합니다. 하지만 해당 방식에는 아쉽지만 몇 가지 치명적인 문제들이 있어요.
우선 비용이 비쌉니다. 가령 {a:1,b:2} {a:3,b:1} 이라는 값을 비교할때 a가 다른게 확인이 되면 b는 안해봐도 되는데 둘다 JSON.stringufy를 안해도 될 비교를 하기 위해 모든 내용을 다 문자로 만들어야 합니다. 큰 객체라면 더욱 더 비효율이 되겠죠.
그리고 JSON.stringify는 객체의 키 순서에 영향을 받을 수 있습니다. {a:1,b:2} {b:2,a:1} 은 사실 같은 객체인데 다르다라고 할 수 있거든요.
그리고 const R = {a: b, self: R} 과 내부에서 재귀적인 참조를 가지고 있다면 JSON.stringify를 하지 못해요.
그리고 flat를 하는 경우 [1, [2]] 와 [1,2] 를 구분하지 못하죠.
간단한 경우에는 문제가 되지 않지만 이러한 core library에서는 엄밀함과 성능을 요구하는 만큼 더 정확하고 빠른 방법으로 찾아가게 되죠.
좋은 경험이 되었기를 바랍니다. 다음 주차도 화이팅입니다!