ai 덜 써보려서 열심히 자료나 소스 줍줍
이번 3주차는 정말 특별한 경험이었습니다. 그동안 Vue를 실무에서 사용해왔던 퍼블리셔로서, React는 "사용법만 알면 되지"라고 생각했는데, 이번 과제를 통해 React의 내부 동작 원리를 직접 구현해보면서 "이게 어떻게 돌아가는 거지?"라는 궁금증을 해소할 수 있었습니다.
처음에는 useRef
, useMemo
같은 것들을 useState
부터 하나하나 직접 만들어보라고 하니까 "이게 뭐야?" 싶었습니다. Vue나 React에서는 이런 것들을 알아서 해주니까 신경 쓸 일이 없었는데, 이번 과제에서는 그런 기능들을 직접 구현해보라고 하니까 처음에는 정말 어려웠습니다.
Vue의 Vuex나 Pinia, React의 Redux나 Zustand 같은 상태관리 라이브러리 등이 해주던 일들을 직접 구현해보니, React의 동작 원리를 훨씬 깊이 이해할 수 있었습니다.
그리고 이번 주 주말에는 두 팀이 페어가 되어 첫 오프라인 만남을 가졌습니다. 지난주 과제로 코드리뷰를 하고, AI 사용 관련 토론을 한 후 토론 내용을 바탕으로 발표해서 상금도 받았습니다. 저녁을 먹으며 모든 팀들과의 게임대결에서 1등해서 또 상금을 받고, 종료 후 2차 회식에서 토론으로 받은 상금으로 회식을 했는데, 이런 과정들을 통해 정말 즐거웠고 친목도 다질 수 있었습니다.
useRef
를 직접 구현해보면서 렌더링이 되어도 참조값이 유지되는 원리를 알 수 있었습니다. useMemo
를 구현하면서 의존성 비교 기반 메모이제이션의 원리를 이해할 수 있었고, 강의에서 배웠던 훅들이 useState
기반으로 동작한다는 것을 알게 되었습니다.
// useRef 구현 - 렌더링되어도 참조값 유지
export function useRef<T>(initialValue: T): { current: T } {
const [ref] = useState(() => ({ current: initialValue }));
return ref;
}
// useMemo 구현 - 의존성 비교 기반 메모이제이션
export function useMemo<T>(factory: () => T, deps: DependencyList, equals = shallowEquals): T {
const prevDeps = useRef<DependencyList>([]);
const prevResult = useRef<T | null>(null);
if (prevResult.current === null || !equals(prevDeps.current, deps)) {
prevDeps.current = deps;
prevResult.current = factory();
}
return prevResult.current;
}
"참조는 고정하되 최신 값은 참조"라는 까다로운 요구사항을 useRef
+ useCallback
조합으로 해결하는 패턴을 알게 되었습니다. 처음에는 타입 때문에 고생했지만, 제네릭과 unknown[]
, as T
타입 단언을 사용해서 TypeScript의 복잡한 타입 시스템을 해결할 수 있었습니다.
export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
const fnRef = useRef(fn);
fnRef.current = fn; // 최신 값 참조
const stableCallback = useCallback((...args: unknown[]) => {
return fnRef.current(...args);
}, []); // 참조 고정
return stableCallback as T;
};
useSyncExternalStore
와 호환되려면 subscribe
함수가 구독 취소 함수를 반환해야 한다는 스펙을 학습했습니다. Observer 패턴을 기반으로 한 상태 관리 시스템을 구현하면서 상태 변경 시 구독자들에게 알림을 보내는 방식을 이해할 수 있었습니다.
export const createObserver = () => {
const listeners = new Set<Listener>();
const subscribe = (fn: Listener) => {
listeners.add(fn);
return () => listeners.delete(fn); // 구독 취소 함수 반환
};
const notify = () => listeners.forEach((listener) => listener());
return { subscribe, notify };
};
ToastProvider에서 함수들이 매번 새로 생성되어 ProductCard가 계속 리렌더링되는 문제를 겪어보면서 Context value의 참조 안정성이 얼마나 중요한지 알게 되었습니다. E2E 테스트 통과 과정에서 하나씩 메모이제이션을 추가하다 보니 useMemo를 5번이나 사용하게 됐는데, 이 과정을 통해 메모이제이션의 중요성을 체감했습니다.
// ToastProvider에서 메모이제이션 적용
const { show, hide } = useMemo(() => createActions(dispatch), [dispatch]);
const commandValue = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]);
const stateValue = useMemo(() => ({ message: state.message, type: state.type }), [state.message, state.type]);
1, 2주차에는 AI에 무작정 의존하던 것에서 이번에는 문서를 보고 시도한 다음에 AI한테 질문하는 식으로 더 나아졌습니다. 특히 useSyncExternalStore
나 Observer 패턴 같은 개념들을 직접 찾아보고 이해하려고 노력한 게 도움이 되었습니다. 전달받은 자료들을 보고 AI에서 해달라기보다 뭔지 물어보고 해결하는 과정을 통해 조금 더 많이 남아 뿌듯했습니다.
리액트의 렌더링 과정(렌더-조정-커밋), 메모이제이션의 필요성과 주의점, 컨텍스트와 상태관리에 대한 나의 생각을 정리하면서 흐름에 대해 잘 파악하게 되었습니다. 이론적으로만 알고 있던 개념들을 직접 구현하고 정리해보면서 React가 실제로 어떻게 작동하는지 깊이 이해할 수 있었습니다.
코치님께서 useAutoCallback 구현을 칭찬해주셨는데, 정말 기뻤습니다. 처음에는 타입 때문에 정말 고생했는데, 그 고생이 인정받는다는 게 뭔가 뿌듯했습니다. 메모이제이션에 대한 조언을 받으면서 "아, 이렇게 생각하면 되는구나" 싶었고, useAutoCallback 패턴이 실제로 쓰일 수 있다는 말에 자신감이 생겼습니다.