[1팀 한아름] Chapter 1-3. React, Beyond the Basics#55
[1팀 한아름] Chapter 1-3. React, Beyond the Basics#55areumH wants to merge 19 commits intohanghae-plus:mainfrom
Conversation
| const shallowSelector = useShallowSelector(selector); | ||
| return shallowSelector(router); | ||
|
|
||
| return useSyncExternalStore(router.subscribe, () => shallowSelector(router)); |
There was a problem hiding this comment.
저는 다 변수로 분리해서 useSyncExternalStore에 인자로 전달했는데, 오히려 이렇게 바로 반환해주니까 더 깔끔하네요 👍🏻
| export const useShallowSelector = <T, S = T>(selector: Selector<T, S>) => { | ||
| // 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅을 구현합니다. | ||
| return (state: T): S => selector(state); | ||
| const ref = useRef<S | null>(null); |
There was a problem hiding this comment.
포괄적인 'ref'라는 이름보단 어떤 데이터를 저장하고 있는지 명확하게 표현해주는 변수명으로 하면 더 이해가 잘 될 것 같아요! (ex. state, prevState, etc)
| const ref = useRef<S | null>(null); | ||
|
|
||
| return (state: T): S => { | ||
| const result = selector(state); |
There was a problem hiding this comment.
result도 selectedState 등과 같은 변수명으로 개선햐면 더 이해하기 쉬울 것 같아요!
| export const deepEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| // 기본 타입 | ||
| if (a === b) return true; |
There was a problem hiding this comment.
| if (a === b) return true; | |
| if (Object.is(a, b)) return true; |
Object.is를 이용한 비교가 더 좋을 것 같아요!
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is
Object.is()는 === 연산자와도 같지 않습니다. Object.is()와 ===의 유일한 차이는 부호 있는 0과 NaN 값들의 처리입니다. === 연산자(및 == 연산자)는 숫자값 -0과 +0을 같게 처리하지만, NaN은 서로 같지 않게 처리합니다.
| <ToastStateContext.Provider value={stateContextValue}> | ||
| <ToastCommandContext.Provider value={actionContextValue}> |
There was a problem hiding this comment.
마이너) ToastStateContext 가 리렌더링을 더 유발하니까 아무래도 ToastStateContext가 안쪽에 있는게 더 낫지 않을까 하는 의견 내봅니다.
There was a problem hiding this comment.
기껏 분리해놓고 중요한 부분을 놓쳤네요 😅 지적 감사합니다!!!
과제 체크포인트
배포 링크
https://areumh.github.io/front_6th_chapter1-3/
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
기술적 성장
확실히 학습 자료를 먼저 읽고 과제를 시작하니 이번 과제에서 이뤄야하는 것이 뭔지, 어떻게 구현해야 하는지 감히 잡히는 것 같다. e2e 테스트 통과를 위해 ToastProvider 코드를 수정할 때 제일 많이 느꼈다..
그냥 '이런 훅이 있구나' 하고 아무 생각 없이 사용했던 리액트 훅들을 직접 구현해보면서 왜 이 훅이 필요한지, 어떻게 동작하는지, 언제 사용해야 하는 지에 대한 감이 생긴 것 같다. 단순히 '이 훅은 이런 인자를 넘겨줘야 한다' 라는 개념을 넘어서 훅 내부에서 어떤 로직으로 상태 변화가 일어나는지, 어떤 타이밍에서 값이 갱신되는지, 리렌더링에 어떤 영향을 주는지 이해할 수 있었다.
직접 구현한 훅이 다른 훅 내부에서 사용되며 하나씩 연결되는 게 너무 신기했다. 독립적으로 사용이 가능하면서도, 서로 연결되어 복잡하면서도 깔끔한 훅을 만들어 내는 것이 재밌었다. 비밀을 파헤친 느낌.!!
자랑하고 싶은 코드
개선이 필요하다고 생각하는 코드
shallowEquals와deepEquals함수에서 string, number, boolean과 같은 기본 타입 비교 구문을if (a === b) return true;로 단순하게 구현하였는데 올바른 방식인지 확실치 않다.그리고 비교하는 값이 객체일 때 각 객체의 key 값을 문자열로 사용하기 위해
Record<string, unknown>와 같이 Record을 사용하였는데, 이 외에 객체를 비교하는 다른 방법이 있는지 궁금하다. solution 코드에는 어떻게 구현되어있을 지 제일 궁금한 부분이다..!!학습 효과 분석
기존의 useCallback 함수는 의존성 배열을 넘겨주어야 하는데, 값을 빠뜨릴 경우 너무 오래된 값을 사용하게 되거나, 너무 많이 넣을 경우 과도한 리렌더링을 유발하는 문제가 생길 수 있다.
useAutoCallback은 이러한 문제를 덜기 위해 의존성 배열을 넘겨주지 않아도 항상 최신값을 유지하도록 하는 훅이다.callback은 의존성 배열이 비어있기 때문에 최초 한 번만 생성되고, 최신 ref의 함수를 호출한다.ref.current = fn;에서 매 렌더마다 ref의 함수가 갱신되기 때문에 고정된 callback 내부에서는 항상 최신 상태의 함수를 참조하게 된다!이로 인해 의존성 배열을 신경쓸 필요 없이, 매 렌더링 시점의 함수 로직을 유지하면서 불필요한 함수 재생성을 방지할 수 있다.
이는 컴포넌트 최상위 레벨에 호출하여 외부 저장소(store)의 상태를 구독하고 읽는 훅이다.
subscribe
외부 저장소의 변경을 구독하는 함수
getSnapShot
현재 저장소 데이터 상태를 반환하는 함수
getServerSnapShot
서버 사이드 렌더링(SSR) 환경에서 사용되는 데이터의 초기 상태를 반환하는 함수 (선택)
store로 전역 상태를 만들고, 변경, 구독할 수 있는 관리 시스템을 구성하는 함수이다. 상태를 변경하는 함수인 reducer과 초기 상태인 initialState를 인자로 받는다.
state는 현재 store의 상태를 저장해두는 변수이며,dispatch를 호출할 때마다 바뀐다. 그리고subscribe는 store의 상태 변화를 감지하는 함수,getState는 현재 store의 상태를 반환한다.이렇게 구성된 createStore 함수로 만든 store을 useSyncExternalStore와 함께 사용하면 안전하고 일관되게 전역 상태를 구독할 수 있다!
createContext 함수는
const SomeContext = createContext(defaultValue)와 같은 형태로 컨텍스트를 생성하고, 여러 컴포넌트 간에 데이터를 전역적으로 공유할 수 있게 해준다. 그리고SomContext.Provider와 같이 Provider를 사용하여 값을 하위 컴포넌트에 전달한다.과제 시작 전에 훑어봤던 학습 자료의 코드에서도
.Provider가 사용되었는데, 이미 주어진 ToastProvider.tsx의 코드에서는 Provider가 붙지 않은ToastContext만 사용하여 값을 전달해주고 있었다. 이를 보고 둘 사이에 기능적으로 차이가 있는지 궁금해졌다.그래서 공식 문서를 찾아봤는데, 리액트 19부터는 Context 뒤에 Provider을 붙인 것과 안 붙인 것이 기능적으로 동일하게 작동한다는 것을 알 수 있었다..!!
배포를 위해
pnpm run gh-pages를 실행했더니 터미널에'cp'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.와 같은 에러가 떴다.package.json의 스크립트 명령어인데,
cp는 Unix 기반 환경에서의 shell 명령어이기 때문에 윈도우에서는 작동하지 않는 것이었다... 관련하여 찾아보니 Unix shell 명령어를 Node.js 스크립트에서 사용할 수 있게 해주는 유틸리티 패키지 shx 라는게 있었다!해당 패키지 설치 후, 명령어를
"build": "vite build && shx cp ./dist/index.html ./dist/404.html"로 수정하여 페이지 배포에 성공했다. 과제와 직접적인 연관이 있는 건 아니지만 그래도 하나 더 알게 되었다..!!(cp 대신 copy로 실행하면 된다는 걸 뒤늦게 알았다........)
과제 피드백
사실 리액트에서 제공해주는 useCallback, useMemo와 같은 훅을 사용해본 경험 자체도 많지 않았기 때문에 과제를 시작할 때 많이 막막했던 것 같다. 그치만 useRef부터 차근차근 직접 구현해보면서 메모이제이션 동작 원리에 대해 조금이나마 더 알게 된 것 같다. 너무 좋은 기회였다! 이번 과제가 도움이 되었다고 직접적으로 체감할 수 있는 날이 얼른 오면 좋겠다는 생각이 든다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
memo,useMemo,useCallback등의 훅을 사용할 수 있고, 불필요한 렌더링을 막아 성능을 개선한다.메모이제이션에 대한 나의 생각을 적어주세요.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
리뷰 받고 싶은 내용
이번 과제에서 useCallback과 useAutoCallback 훅을 구현하면서, 편의성과 명시성 측면에서 비슷하면서도 서로 다른 특징을 갖고 있다고 느꼈습니다. 특히
useAutoCallback은 참조를 고정하면서도, 항상 새로운 값을 참조하도록 하는 게 굉장히 편리하다는 생각이 드는데, 아직 useCallback과 useAutoCallback 훅이 각각 어떤 상황에 더 적합한지 확실히 와닿지 않습니다. useAutoCallback을 사용할 때 조심해야 하는 부분이나, 문제가 생길 수 있는 경우가 있을 지 궁금합니다!