과제 체크포인트
배포 링크
https://suhyeon57.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 개선
과제 셀프회고
기술적 성장
const ref = useRef<{ deps: DependencyList; value: T }>({
deps: _deps,
value: factory(), // 매 렌더마다 실행됨!
});
useMemo를 처음에는 이렇게 구현했는데, 매 렌더마다 실행되는 오류를 가지고 있었다. 의존성에 상관없이 실행되니 useMemo의 역할을 하지 못하는 상황이 발생
다시 제대로 구현
const ref = useRef<{ deps: DependencyList; value: T } | null>(null);
if (!ref.current || !_equals(ref.current.deps, _deps)) {
ref.current = {
deps: _deps,
value: factory(),
};
}
useShallowState.js
import { useState, useCallback } from "react";
import { shallowEquals } from "../equals";
export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => {
const [first, setFirst] = useState(initialValue);
const customSetState = useCallback((next: T) => {
setFirst((prev) => {
if (!shallowEquals(prev, next)) {
return next;
}
return prev;
});
}, []);
return [first, customSetState] as const;
};
이 코드에서 타입을 명확하게 지정해서 타입스크립트 오류를 방지 위해 아래처럼 변경
import { useState, useCallback } from "react";
import { shallowEquals } from "../equals";
//얕은 비교를 통해 상태를 관리
export const useShallowState = <T>(initialValue: T) => {
const [first, setFirst] = useState<T>(initialValue);
const customSetState = useCallback((next: T) => {
setFirst((prev) => {
if (!shallowEquals(prev, next)) {
return next;
}
return prev;
});
}, []);
return [first, customSetState] as const;
};
ToastProvider.tsx
export const ToastProvider = memo(({ children }: PropsWithChildren) => {
const [state, dispatch] = useReducer(toastReducer, initialState);
const { show, hide } = createActions(dispatch);
const visible = state.message !== "";
const hideAfter = debounce(hide, DEFAULT_DELAY);
const showWithHide: ShowToast = useCallback((...args) => {
show(...args);
hideAfter();
}, []);
const contextValue = useMemo(() => ({ ...state, show: showWithHide, hide }), [state, showWithHide, hide]);
return (
<ToastContext value={contextValue}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastContext>
);
});
처음 시도한 코드는 useCallback과 useMemo를 설정해줬다. 하지만, Context value 객체가 바뀌면 하위 컴포넌트가 리렌더링 되는 현상이 발생했다. 두 번째 방법으로 useRef로 show/hide를 고정해도, state가 바뀌면 Context value가 바뀌어 리렌더링 되는 문제점을 발견했다. 그래서 action과 state Context를 분리해야 하며, 하위 컴포넌트가 action만 구독하면 show/hide가 변하지 않는 한 리렌더링되지 않는 것을 알게 되었다. state는 토스트 메시지 변화에만 영향을 주도록 분리하고, (state는 메세지가 바뀔 때를 위해 분리를 해주어야 한다.) 하위 컴포넌트들은 action 요소만 구독을 하기 때문에 show와 hide가 변하지 않으면 다시 렌더링이 되지 않는다.
수정 코드
//action context 설정
const ToastActionsContext = createContext<{
show: ShowToast;
hide: Hide;
}>({
show: () => null,
hide: () => null,
});
//state context 설정
const ToastStateContext = createContext<{
message: string;
type: ToastType;
}>({
...initialState,
});
const { show, hide } = useRef(createActions(dispatch)).current; //useRef를 사용하였기 때문에 리렌더링과 무관하게 고정된 값 유지
return (
<ToastActionsContext value={context}>
<ToastStateContext value={state}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastStateContext>
</ToastActionsContext>
);
자랑하고 싶은 코드
아직은 없는 것 같습니다. 리팩토링을 통해 제 코드로 다시 만들 생각입니다.
개선이 필요하다고 생각하는 코드
이번 과제를 통해 실무에서 성능 최적화를 위한 훅 사용법을 더 명확히 이해할 수 있었습니다. 특히, 어떤 상황에서 어떤 훅을 사용해야 하는지를 고민해보는 계기가 되어 좋았습니다. 하지만 아직도 훅들이 언제, 어디서 적절하게 사용되는지에 대한 감각은 부족하다고 느꼈습니다. 이를 보완하기 위해 벨로그에 직접 정리하며 복습할 예정입니다. 이러한 과정을 통해 실무에 더 효과적으로 적용할 수 있을 것이라 기대하고 있습니다.
학습 효과 분석
ToastProvider.tsx (useContext에 대해)
context는 props drilling 없이 컴포넌트 트리 전체에 데이터를 제공
//state context 설정
const ToastStateContext = createContext<{
message: string;
type: ToastType;
}>({
...initialState,
});
//action context 설정
const ToastActionsContext = createContext<{
show: ShowToast;
hide: Hide;
}>({
show: () => null,
hide: () => null,
});
--> 초기값 설정 시 provider 없이 사용할 수 있다. 초기값 없이 사용할 시 provider 컴포넌트를 생성하고, 커스텀 훅에서 예외 처리를 해줘야 한다.
<ToastActionsContext value={context}>
<ToastStateContext value={state}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastStateContext>
</ToastActionsContext>
현재 코드에서 provider 컴포넌트가 없는 이유는 초기값을 설정해주었기 때문이다.
커스텀 훅
export const useToastCommand = () => {
const { show, hide } = useContext(ToastActionsContext);
return { show, hide };
};
export const useToastState = () => {
const { message, type } = useContext(ToastStateContext);
return { message, type };
};
--> 자주 사용하는 로직을 커스텀 훅으로 묶어서 사용한다. 외부에서도 사용할 수 있다. 커스텀 훅으로 사용했기 때문에 외부에서도 아래처럼 사용이 가능하다.
const toast = useToastCommand();
과제 피드백
React Hook을 실무에서 사용할 때에는 제대로 알지 못하고 쓰는 기분이 들었는데, 어떤 부분에서 렌더링이 최적화 되는지 깊게 파악할 수 있어서 좋았습니다. 그리고, 과제에서 다룬 내용을 실무에도 바로 적용해 보았습니다. 기존 윈도우 크기 추적 로직을 useSyncExternalStore를 활용해 리팩토링했고, 보다 안정적이고 예측 가능한 방식으로 개선할 수 있었습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
React의 렌더링은 Virtual DOM(가상 DOM) 을 기반으로 동작합니다. 컴포넌트의 상태(state)나 props가 변경되면, React는 변경된 값을 기반으로 새로운 Virtual DOM을 생성합니다.
이후 이전 Virtual DOM과 새 Virtual DOM을 비교(diff 알고리즘)하여 변경된 부분만 찾아 실제 DOM에 최소한으로 반영합니다. 이 과정을 통해 DOM 조작의 비용을 줄이고 성능을 최적화합니다.
또한 React에서는 useMemo, useCallback, React.memo 등과 같은 메모이제이션 기법을 통해 불필요한 렌더링을 줄일 수 있습니다.
그리고 useEffect 등의 Hook을 통해 렌더링 이후의 사이드 이펙트 처리를 제어할 수 있으며, 이 역시 렌더링 흐름에서 중요한 역할을 합니다.
메모제이션에 대한 나의 생각을 적어주세요.
메모이제이션은 동일한 연산이 반복될 때 성능을 최적화할 수 있는 중요한 기법이라고 생각합니다. 이를 사용하지 않으면 불필요한 렌더링이나 계산이 발생할 수 있고, 결과적으로 앱 성능에 영향을 줄 수 있습니다.
특히 useMemo는 계산 비용이 큰 연산 결과를 캐싱하는 데 유리해서, 단순히 값을 보존하는 useRef보다 성능 측면에서 더 적절할 수 있습니다.
실제로 ToastProvider 과제를 진행하면서 메모이제이션을 통해 리렌더링을 줄이고 성능을 향상시킬 수 있다는 점을 체감했습니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
Context는 props drilling 없이 컴포넌트 트리 전체에 데이터를 전달할 수 있게 해주는 기능입니다. 이를 통해 상위 컴포넌트에서 하위 컴포넌트로 일일이 props를 전달하지 않아도 되므로 코드가 간결해지고 유지보수가 쉬워집니다.
또한 Context와 상태관리 도구를 함께 사용하면 전역 상태를 효과적으로 관리할 수 있습니다. 적절하게 분리하지 않으면, 하위 컴포넌트가 불필요하게 자주 리렌더링되는 문제가 발생할 수 있습니다.
따라서 Context는 단순히 데이터를 공유하는 역할을 넘어, 상태관리 전략과 함께 사용함으로써 리렌더링을 최소화하고, 더 효율적인 앱 구조를 만드는 데 큰 도움이 된다고 생각합니다.
리뷰 받고 싶은 내용
export const ToastProvider = memo(({ children }: PropsWithChildren) => {
const [state, dispatch] = useReducer(toastReducer, initialState);
const { show, hide } = useRef(createActions(dispatch)).current; //useRef를 사용하였기 때문에 리렌더링과 무관하게 고정된 값 유지
const visible = state.message !== "";
const hideAfter = debounce(hide, DEFAULT_DELAY);
const showWithHide: ShowToast = useCallback((...args) => {
show(...args);
hideAfter();
}, []); //의존성 배열을 빈 배열로 설정하여 최초 렌더링 시에만 실행되도록 함
const context = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]); //showWithHide와 hide가 변하지 않으므로 변하지 않음
return (
<ToastActionsContext value={context}>
<ToastStateContext value={state}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastStateContext>
</ToastActionsContext>
);
});
저는 아래 코드에서 createActions(dispatch)로 만든 show, hide 함수를 useRef로 감싸서 리렌더링과 무관하게 고정된 값으로 유지하고, 이후 useCallback 안에서 사용하고 있습니다. 이런 방법을 통해 useCallback에서 의존성 배열을 빈 배열로 두어도 lint 경고가 생기지 않는데, 이 방법을 사용해도 되는지 궁금합니다. (현재는 팀원들의 도움으로, useMemo, useCallback 방법으로 변경하였습니다.)
이번 과제를 통해 React Hook이 어떤 역할을 하고, 어떻게 사용되는지에 대해 감을 잡을 수 있었습니다. 하지만 아직도 어떤 상황에서 어떤 Hook을 써야 하는지, 그리고 성능 최적화를 위해 어떤 판단 기준을 가져야 할지는 여전히 모호하게 느껴집니다. 혹시 Hook을 더 잘 이해하고 적재적소에 활용하기 위해 추천하시는 학습 방법이나 연습 방식이 있다면 알려주시면 감사하겠습니다!
과제 피드백
수현님 고생하셨습니다! 필요한 내용은 전부 잘 작성해주셨고, 부족하다고 생각이 드시는 부분은 꼭 말씀해주신것처럼 본인의 것으로 만들어보시면 좋겠네요 ㅎㅎ 회고도 잘 작성해주셨고, 따라가면서 읽어보니 실제 어떤 고민들을 하셨는지 직접적으로 잘 이해할 수 있었습니다.
이어서 남겨주신 질문 답변 답변해보면요!
show, hide 함수 useRef관련
우선 팀원들이 알려주신 방식으로 변경하는게 좀 더 추적이 잘 되고 관리가 잘 되는 방식입니다 ㅎㅎ 절대! 안변하고 명시적으로 관리할 수 있다면 지금 useRef내에서 사용하는것도 방법이겠지만, 함께 사용되는 dispatch자체에 대한 참조가 변경되는 경우 문제가 충분히 발생할 여지가 있어보여요. useRef에는 일반적으로 함수를 저장하는것은 권장되지 않는 패턴인 것 같아요!
훅 사용
훅은 말이해하셨던것처럼 리액트 라이프사이클을 타는 컴포넌트가 아닌 다른 것을 반환하는 함수 자체로 바라봐보면 좋을 것 같아요. 이를 통해서 리액트 컴포넌트 내부에서만 할 수 있었던 여러 부수효과를 다룬다거나 리액트 라이프 사이클 안에서 동작하는 여러 기능들을 활용할 수 있게 되잖아요. 이를 통해서 반복되는 여러 상태들을 분리해서 하나로 모아 하나의 관심사를 분리해 재사용성을 높일수도 있고 코드의 가독성을 높일수도 있겠죠. 추가로 비즈니스 로직들이 여러곳에서 사용되고 있을 때 이런 부분들을 한 곳에서 추상화 해 가독성을 높이고 응집성을 높일수 있겠죠. 그리고 성능 관점에서도 리렌더링이나 성능적으로 병목이 발생하는 지점들을 한 곳에 모아놓고 특별 관리를 할 수 있고 이런 부분들은 결국 테스트의 용이성으로 이어져서 관리하기 편한 프로젝트를 만들어 주게 되는 것 같아요.
추가로 훅을 통해 DOM이나 BOM을 래핑해 유틸성 기능들을 제공하는 여러 저장소도 있는데 함께 찾아 살펴보는 것도 좋을 것 같구요!
고생하셨고 다음 주도 화이팅이에요~~