과제 체크포인트
배포 링크
https://k-sang-soo.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 도움 없이도 빨리 풀었다. 너무 짜릿했다 도파민 폭발! 그리고 이번에는 최대한 모든 문제해결의 모든 과정들을 문서화하려고 노력했다 향해 3주차에 이제서야 어떻게 공부를 해야될지 사아알짝 감이 잡힌것 같다.
기술적 성장
-
useState, useRef
useState를 그냥 아무 생각 없이 상태를 반응형으로 만들려고 사용했고 더 깊이 생각해보질 않았는데, useRef를 구현할 때 useState가 사용된다는 걸 보고,useState를 통해 몰랐던 리액트의 동작 원리들을 알게 돼서 너무 좋았습니다. lazy initialization 에 대해서도 알게 되고 어떻게 사용되는지 lazy initialization를 적용하지 않았을 때는 어떤 차이점이 있는지 알게 됐습니다. -
useMemo, useCallback
useMemo를 구현할 땐useRef를 사용하고,useCallback을 구현할 땐useMemo를 사용한다는 걸 알게 됐습니다. 리액트는useState와useRef가 굉장히 중요한 hook 이였구나 깨달았습니다. 그리고useMemo로 상태를 비교할 때 깊은 비교를 하지 않는 이이유와 리액트가 참조 비교를 원칙으로 하고 왜 그런지 깨닫게 됐습니다. -
useShallowState, useAutoCallback 참조 비교가 아닌 얕은 비교, 깊은 비교를 사용한다면 참조 비교와는 다르게 불필요한 리렌더링을 줄일 수 있구나 알게 됐습니다.
-
useSyncExternalStore 리액트 18에서 새롭게 생긴
useSyncExternalStore를 사용해서 외부 상태를 구독해서 사용하는 방법을 깨달았다. 단순히 브라우저 API를 사용할 때 쓰면 좋다 라고만 인지하고 있었는데 실제로 써보고 어떤 문제를 해결하려고 나왔는지 찾아보니 유용하게 쓸 수 있는 hook 이라는 걸 깨달았습니다.
자랑하고 싶은 코드
자랑하고 싶은 "코드"보다는 이번 과제를 제대로 습득하기 위해서 어떤 고민과 생각을 하면서 문제를 해결했는지 열심히 문서화하려고 노력했던 점이 스스로 자랑스럽다고 생각합니다! 정리를 잘했다고 보기는 어렵지만 어떻게 공부해야 될지 조금 깨달은 것 같고 향해를 하면서 공부하는 방법을 더욱 더 깨닫길 기원하면서 열심히 써봤습니다!
여기에 제 모든 정수를 담았습니다!!!! 이번 과제 총 정리
칭찬해주고 싶은 동료
-
5팀의 이지훈 제가 useMemo로 한참 고민하고 있을 때 비록 다른 팀이지만 지훈님께서 "교수님"이라는 타이틀을 얻을 정도로 잘 설명해준다고 해서 여쭤보러가니 아주 흔쾌하게 시간을 내주시고 과제에 대한 핵심 개념과 힌트를 주셔서 풀 수 있게 됐습니다!!
-
2팀의 유운우 사전 스터디 때 말씀을 나눠보니 윤우님께서 아는 게 많고, 실력도 뛰어나다는 인상을 받았습니다. 그래서 비록 다른 팀이지만 도움을 요청했고 흔쾌하게 시간을 내주셨습니다. 덕분에
useShallowState와 심화 과제의 마지막 문제ToastProvider최적화를 할 수 있게 됐습니다!
저도 이 두분에게 받은 좋은 시너지를 저희 팀에게 전달하고 싶어 제가 문서화한걸 공유해보기도 했습니다! 함께 성장하는 향해!!
개선이 필요하다고 생각하는 코드
개선이 필요하거나 코드 관련 고민은 리뷰 받고 싶은 내용 쪽에 적은 것과 동일합니다!
학습 효과 분석
-
가장 큰 배움이 있었던 부분 리액트의 근간이 되는
useState에 대해 깊게 이해할 수 있었고, 단순히 상태를 반응형으로 만드는 도구로만 생각했었는데, 지금 생각하면 왜 이런 생각을 못했을까 라고 생각했던 리렌더링 사이에서도 값을 유지하는 원리를 알게된게 큰 수확이었습니다. 이 개념을 통해서 모든 hook들이 파생되는 것 같아 가장 큰 배움이었습니다. -
추가 학습이 필요한 영역
useState도 직접 구현해볼 수 있었으면 좋았을 것 같습니다. 물론 과제하면서 시간 남으면 해봐야지 라고 생각했지만 시간이 없었습니다ㅠㅠ 나중에 꼭 직접 구현해보겠습니다. -
실무 적용 가능성 코드들에 대한 실무 적용 가능성보다 리액트의 동작 원리들을 이해했고 이런 동작 원리들을 바탕으로 설계할 때나 디버깅할 떄 매우 유용할 것 같습니다!
과제 피드백
- 정답이 어느정도 정해져 있고 그 안에서만 고민할 수 있어서 집중하기 좋았습니다!
useState를 직접 구현하는 것도 포함 되어있었더라면 좋았겠다 라고 생각했습니다.useRef를 만들기 위해서는useState가 필요하니 순서대로 생각하면 있는게 좋지 않았을까 생각이 듭니다. 아마도 코치님들께서도 분묭 생각 하셨을 것 같은데 어떤 이유 때문에 빠졌을 지 궁금합니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리액트는 jsx 라는 특별한 문법을 통해 UI 구조를 선언하고, 이를 트랜스파일러가 React.createElement 호출로 ReactElement 객체를 만드는데, 이 때 객체를 가상 DOM이라고 불리는 내부 구조로 사용된다.
최초 렌더링 시에는 이 Virtual DOM을 기반으로 실제 DOM을 생성하고, 이 후 props 나 state 가 변경되면 리렌더링이 일어나고, 변경된 Virtual DOM과 이전 Virtual DOM을 diffing 알고리즘을 통해 달라진 걸 비교해서 달라진 부분만 DOM에 반영한다. 이 과정을 Reconciliation 이라 부른다.
리액트는 최적화하기 위해서는 왜 리렌더링이 일어나는지를 먼저 알아야 한다. 리렌더링이 일어나는 경우는 props, state, 부모 컴포넌트가 리렌더링되면 일어난다. 이 때 변수나 함수를 참조 비교를 하게 되는데 이전과 동일하지 않는다면 변경이 일어난다. 변경이 일어나지 않게 하기 위해서는 변수나 함수를 useMemo, useCallback, memo와 같은 메모이제이션 도구를 활용해 참조를 유지해주고, 의존성 값이 변했을 때만 새로 생성하게 해야된다.
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션에 대해서 항상 코드를 짤 때 적용해줘야 되나? 많은 고민들이 있었지만 리액트 공식문서나 여러 개발자들의 의견을 봤을 때 정말 필요하다고 느껴졌을 때 적용하라는 의견을 참고하여 꼭 필요하다고 느껴질 때만 적용하는 걸로 결정했다. 불필요한 리렌더링이 되는 것 처럼 보이지만 문제가 일어나지 않은 상황에서 이를 막기 위해 메모이제이션을 사용한다면 그것 또한 리소스를 잡아 먹기 때문에 정말 문제가 있을 때만 도입하자!
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
컨텍스트를 사용하면 깊은 하위 컴포넌트까지 데이터를 전달해주고, 여러 컴포넌트들 간의 상태를 공유할 수 있어서 좋다. 하지만 컨텍스트를 사용하면 코드의 구조가 복잡해져서 읽기가 어려워지고, 상태가 변경되면 구독하고 있는 모든 컴포넌트가 리렌더링 되는 구조 때문에 최적화가 필요하다면 최적화를 진행하기 까다롭다. Zustand, Jotai 같은 최적화도 잘 되어있고 사용하기도 쉬운 전역 상태 라이브러리들이 나와있어 컨텍스트를 사용할 일이 있을까 싶기도 하다. 물론 외부 라이브러리의 의존성이 없고 리액트의 api를 사용한다는게 장점이 될 수도 있지만!
리뷰 받고 싶은 내용
useShallowState의 타입 부분 질문 드립니다!
처음에는 타입을 원래 구현 되어있던 <T>(initialValue: Parameters<typeof useState<T>>[0]) 을 유지한 채 구현을 시도했습니다.
export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => {
const [state, setState] = useState(initialValue);
const setValueShallow = useCallback((newValue: T) => {
setState((prevValue: T) => {
return shallowEquals(prevValue, newValue) ? prevValue : newValue;
});
}, []);
return [state, setValueShallow] as const;
};
그런데 setState((prevValue: T) => { 부분에서 아래와 같은 타입 에러가 발생했습니다.
TS2345: Argument of type (prevValue: T) => T is not assignable to parameter of type SetStateAction<undefined>
Type 'undefined' is not assignable to type 'T'
제가 이해한 바로는, T에 undefined가 들어올 가능성을 타입스크립트가 열어두면서 undefined로 추론하게 되는 케이스까지 포함된 것 같습니다.
그래서 결국 initialValue가 들어오는 순간 T 타입을 확정 하는 <T>(initialValue: T | (() => T)) 으로 해결하긴 했습니다.
export const useShallowState = <T>(initialValue: T | (() => T)) => {
const [state, setState] = useState(initialValue);
const setValueShallow = useCallback((newValue: T) => {
setState((prevValue) => {
return shallowEquals(prevValue, newValue) ? prevValue : newValue;
});
}, []);
return [state, setValueShallow] as const;
};
<T>(initialValue: Parameters<typeof useState<T>>[0]) 을 사용하더라도 안전하게 타입 추론을 맞추는 방법이 있는지 여쭤보고 싶습니다!
useAutoCallback의 타입 부분 질문 드립니다! 예를 들어fn이(name: string, age: number) ⇒ string형태일 때useCallback안 의 args 타입을 맞추기 위해(...args: Parameters<T>): ReturnType<T>이렇게 타입을 지정했습니다. 이런 식으로 작성하면 타입이 정확한 매칭이 되지 않아 에러가 발생해서, 결국 마지막에as T로 타입 단언을 해야지만 에러가 사라졌습니다. 근데 이렇게 타입 단언 선언을 하는 것이 괜찮은 방법인지 의문이 듭니다. 강제로 맞춘 느낌이 들고 any를 사용한것 같은 느낌이 들어 찝찝합니다. 더 안전하게 타입을 맞추는 방법이 있을지 궁금합니다!
export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
const fnRef = useRef(fn);
fnRef.current = fn;
return useCallback((args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
}, []) as T;
};
-
useShallowState나useAutoCallback같은 훅들의 목적은 괜찮다고 생각은 들지만, 리액트는 기본적으로 참조 비교로 리렌더링 여부를 판단하고, 이런 동작은 성능을 위한 최적화이기도 하지만, 예측 가능한 동작과 불변성을 유지하기 위한 철학이라고 알고 있습니다. 그래서useShallowState나useAutoCallback처럼 동작을 바꾸는 훅을 그냥 써도 될까? 라는 고민이 들었고 리액트 철학과 어긋나는 건 아닐까 싶은 생각이 들었습니다. 혹시 실무에서 자주 사용하시거나 특정 상황에서만 제한적으로 사용하는 편이신가요? 예를 들어 어떤 상황에서 사용할 수 있을까요? 사용하신다면 사이드 이펙트가 생긴 적은 없나요? 깊은 지식이 부족해서 이게 써도 괜찮은 도구인지 판단이 잘 안서서 여쭤봅니다! -
고차 컴포넌트나 고차 함수는 잘 활용하면 코드의 유연성과 재사용성 측면에서 굉장히 유익하다고 생각합니다. 하지만 막상 이런 방식으로 작성된 코드를 읽을 때는 한 번에 이해하기가 어려운 경우가 많습니다. 개인적으로 좋은 코드는 읽기 쉬운 코드도 중요한 요소라고 생각하는데, 그런 점에서 고차 컴포넌트나 고차 함수가 반드시 좋은 코드라고 말할 수 있을 지 고민됩니다. 그냥 제가 익숙하지 않아서 그렇게 느끼는 걸까요? 실무에서도 이런 고차 컴포넌트나 고차 함수를 자주 사용하시는지, 만약 신입 개발자가 새로 들어온다면 이런 구조를 바로 이해하기는 어려울 것 같은데 이런 경우에는 어떻게 도와주거나 가이드하시는 지 궁금합니다!
과제 피드백
안녕하세요 상수님! 수고하셨습니다. 이번 과제는 React의 내장 훅들을 직접 구현해보면서 프레임워크의 동작 원리를 깊이 이해하는 것이 목표였습니다. AI 없이 최대한 고민해보시고 뇌를 괴롭혀가며 문제를 해결하신 과정 정말로 칭찬드립니다. 1챕터는 결과를 만드는 게 아니라 체험을 통해 학습을 하는 과정인 만큼 힘들었겠지만 좋은 경험이 되었을거라 생각합니다. 또한 "모든 문제해결 과정을 문서화하려고 노력했다"는 점에서 진정한 개발자의 자세가 느껴집니다.
코드를 살펴보니 솔루션과 비교했을 때 핵심 개념들을 잘 이해하고 구현해주셨어요. 특히 equals 함수들을 별도 파일로 분리하고 typeUtils까지 만들어서 깔끔하게 정리하신 점이 좋았습니다. baseEquals 패턴을 사용해서 코드 중복을 줄이신 것도 훌륭한 접근입니다!
Q) useShallowState의 타입 문제
useShallowState의 타입 문제는 실제로 타입스크립트의 복잡한 부분이에요. Parameters<typeof useState
useAutoCallback의 타입 단언도 어쩔 수 없는 부분입니다. 타입스크립트가 고차 함수의 복잡한 타입 추론을 완벽하게 처리하지 못하는 한계가 있어서, 안전한 범위 내에서의 타입 단언은 실무에서도 허용되는 패턴이에요.
정리하자면, 가급적 타입추론을 활용할수 있도록 하는건 좋은 방법입니다. 그러나 내가 API를 제공하는 layer를 만들고 안전하게 타입을 처리할 수 있는 경우라면 이걸 쓰는 쪽에서 타입 추론을 더 잘할 수 있도록 단언등을 통해서 간결화 해주는것이 좋습니다.
useAutoCallback의 경우에도 마찬가리로 (더 나은 방식이 있을지는 고민을 해봐야겠지만) 함수를 제공하는 쪽의 타입들은 코드상 안전하다면 오히려 타입 단언을 해주는 편이 좋습니다.
Q) useShallowState 나 useAutoCallback은 아시다시피 공식적인 방식이 아니라 편의를 이용한 방법이죠. 사용여부는 전적으로 개발자의 취향이라고 생각합니다. 고민이 든다면 굳이 쓰지 않아도 괜찮다고 생각을 합니다. 내가 배운거나 필요한 모든 것들을 다 써야 하는건 아니니까요. 이러한 개발에 대한 철학적 사유를 해보는 것들 너무 좋습니다. 저도 둘다 실무에서 쓴 적은 없습니다.
useAutoCallback의 경우 그냥 핸들러를 바로 자식컴포넌트의 props로 넘기게 되면 React.memo가 될 수 없는데 이 방식을 쓰면 의존성 배열 없이 props도 참조를 유지할 수 있는 해법이겠네요.
개인적으로는 컴포넌트의 props에 함수를 전달하는 것을 최대한 지양하는 방식으로 만들고 있어서 useCallback을 잘 안쓰고 있어요. props를 많이 사용해서 독립성있고 투명하게 만드는 것도 props drill을 최소화 하는것도 취향의 영역이니 본인만의 취향을 가져보세요. 단 말씀해주신대로 깊이를 통해서 취향에 대한 확신과 근거도 가져보면 좋겠습니다
Q) 고차 컴포넌트에 대한 고민도 공감합니다. 처음엔 복잡해 보이지만 패턴에 익숙해지면 코드의 재사용성과 유연성 측면에서 큰 도움이 되요. 추상화의 가장 큰 문제는 구현부가 사라진다는 거죠. 그렇기에 널리 알려진 이름, 내부 동작이 예측가능하도록 만들어두는 것이 중요하죠. 추상화는 언제나 그런 트레이드 오프를 가져옵니다. 네이밍과 문서화가 그래서 중요한거겠죠. 처음에는 안 읽혀도 알고 나면 쉬워지니까요.
클린코드 시간에 지금과 같은 고민들을 충분히 해보시길 바래요! 수고하셨습니다. 다음 주차도 화이팅입니다!