과제 체크포인트
배포 링크
https://geonhwiii.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가 왜 이런 API를 제공하는지 이해할 수 있었습니다. 복잡한 내부 로직을 간단한 API로 감싸주는 추상화로 인해 사용자가 얼마나 편하게 사용할 수 있는지 배웠습니다.
또, React에서 제공하는 useState를 제외한 api는 custom hook의 일종이라는 것도 깨달았습니다.
기술적 성장
useRef가useState의lazy initialization을 활용한다는 원리를 이해useCallback과useMemo의 관계- 얕은 비교와 깊은 비교의 내부적인 동작에 대한 이해
자랑하고 싶은 코드
// useRef.ts
import { useState } from "react";
interface MutableRefObject<T> {
current: T;
}
/**
* 1. DOM 요소 null 초기화 대응
* const ref = useRef<HTMLDivElement>(null)
*
* 2. 초기값이 없는 경우 대응
* const ref = useRef<string>();
*/
export function useRef<T>(initialValue: T): MutableRefObject<T>;
export function useRef<T>(initialValue: T | null): MutableRefObject<T | null>;
export function useRef<T = undefined>(): MutableRefObject<T | undefined>;
export function useRef<T>(initialValue?: T): MutableRefObject<T | undefined> {
const [ref] = useState(() => ({ current: initialValue }));
return ref;
}
실제 useRef사용과 비슷하게 사용할 수 있도록 타입 추론을 보완했어요.
추가적인 타입 선언이 없을 경우,
const ref = useRef<HTMLDivElement>(null)는 타입 오류가 발생해요.
// deepEquals.ts
export const deepEquals = (a: unknown, b: unknown) => {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
// 각 요소를 재귀적으로 비교
for (let i = 0; i < a.length; i++) {
if (!deepEquals(a[i], b[i])) return false;
}
return true;
}
if (typeof a === "object" && typeof b === "object") {
const objA = a as Record<string, unknown>;
const objB = b as Record<string, unknown>;
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
// 각 키의 값을 재귀적으로 비교
for (const key of keysA) {
if (!deepEquals(objA[key], objB[key])) return false;
}
return true;
}
return false;
};
각 조건문이 의미하는 바가 명확하고, 이해가 간단하다고 생각해요.
개선이 필요하다고 생각하는 코드
// AS-IS
export function useShallowState<T>(initialValue: T | (() => T)): [T, React.Dispatch<React.SetStateAction<T>>] {
const [state, setState] = useState(initialValue);
const setShallowState = useCallback((newValue: T | ((prev: T) => T)) => {
setState((prevState) => {
const nextState = typeof newValue === "function" ? (newValue as (prev: T) => T)(prevState) : newValue;
// 1. 얕은 비교 값이 같다면 이전 상태 반환
if (shallowEquals(nextState, prevState)) {
return prevState;
}
// 2. 값이 다르다면 새로운 상태 반환
return nextState;
});
}, []);
return [state, setShallowState];
}
// TO-BE
export function useShallowState<T>(initialValue: T | (() => T)): [T, React.Dispatch<React.SetStateAction<T>>] {
const [state, setState] = useState(initialValue);
const setShallowState = useCallback((newValue: T | ((prev: T) => T)) => {
setState((prevState) => {
const nextState = isFunction(newValue) ? newValue(prevState) : newValue;
// 1. 얕은 비교로 값이 같다면 이전 상태 반환
if (shallowEquals(nextState, prevState)) {
return prevState;
}
// 2. 값이 다르다면 새로운 상태 반환
return nextState;
});
}, []);
return [state, setShallowState];
}
function isFunction<T>(value: T | ((prev: T) => T)): value is (prev: T) => T {
return typeof value === "function";
}
강제 타입캐스팅을 강제하는 부분을 줄이고 싶었어요.
타입 가드 함수를 활용해서, 강제 타입 캐스팅을 줄이고, 타입 안정성을 보완할 수 있을 것 같아요.
학습 효과 분석
이전의 사고방식:
- React Hooks는 그냥 주어진 API로만 생각하였어요.
- "어떻게 쓸까?"에만 집중하였어요.
현재의 사고방식:
- "왜 이렇게 설계되었을까?"를 항상 고민하게 되었어요.
- "내부적으로 어떻게 동작할까?"를 생각할 수 있게 되었어요
과제 피드백
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리렌더링이 발생하는 조건:
- 상태 변경: 상태 비교로 이전 값과 다를 때
- Props 변경: 부모로부터 받은 props가 변경될 때
- Context 변경: 구독 중인 Context 값이 변경될 때
렌더링 최적화
- 불필요한 리렌더링 방지:
// ❌ 매번 새로운 객체 생성
<Component style={{ margin: 10 }} />
// ✅ 참조 안정화
const style = useMemo(() => ({ margin: 10 }), []);
<Component style={style} />
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션은 비용이 많이 드는 곳에 사용하면 이점을 갖지만, 남용을 하게 될 경우, 코드의 복잡성이 증대될 수 있어요.
최근 리액트 컴파일러 발표 영상을 보면, 리액트 개발팀조차도 메모이제이션을 실수할 때도 있다고 언급한 부분을 보았을 때, 쉬운 영역이라고도 생각이 들지 않아요.
의도를 명확히 사용하고, 유지보수성을 고려해서 사용하면 좋을 것 같아요.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
FSD 아키텍처가 유행하는 이유는 대규모 아키텍처에 대한 고민도 있지만,
도메인, 기능에 대해서 관리 포인트를 가장 가까운 곳에서 관리하려는 것도 있다고 생각해요.
이와 마찬가지로, 상태도 가장 가까운 곳에서 관리하는 것이 중요해요.
컨텍스트는 Provider를 통해 의존성을 깊게 주입해야 하는 상황에서 사용하면 좋을 것 같아요.
useSyncExternalStore의 등장으로 리렌더링을 줄이면서, selector를 통해 선택적으로 상태에 대해 구독을 할 수 있게 되었어요.
따라서, 개발자는 항상 비용을 고려해서 사용할 수 있도록 노력해야해요.
리뷰 받고 싶은 내용
useSyncExternalStore에 대한 이해가 아직은 부족한 것 같습니다.
이번 과제에서 사용한 useStore와 같은 store 외에 활용 방법이 있을까요?
그리고, useSyncExternalStore를 단일로 사용하는 것보다
context api와 결합해서 사용하는 패턴이 더 좋은 것일지,
상황에 따라 선택하는 것이 맞을지 궁금합니다.
과제 피드백
건휘님 이번에도 무난히 합격하셨네요 :) 과제 MR에 회고 내용이 없던데(금요일 오후7시 기준) 제가 뭔가 잘못본건 아닌지 모르겠네욥. 내용을 보면 각 모듈을 스펙을 md로 만들어뒀던데 호오.. 이건 뭔죵 ㅎㅎ 수고많으셨습니다!