Skip to content

Conversation

@hyodduru
Copy link

@hyodduru hyodduru commented Apr 10, 2025

과제 체크포인트

배포 링크

https://hyodduru.github.io/front_5th_chapter1-3/

기본과제

  • shallowEquals 구현 완료
  • deepEquals 구현 완료
  • memo 구현 완료
  • deepMemo 구현 완료
  • useRef 구현 완료
  • useMemo 구현 완료
  • useDeepMemo 구현 완료
  • useCallback 구현 완료

심화 과제

  • 기본과제에서 작성한 hook을 이용하여 렌더링 최적화를 진행하였다.
  • Context 코드를 개선하여 렌더링을 최소화하였다.

과제 셀프회고

기술적 성장

이번 과제를 진행하면서 React 컴포넌트의 성능 최적화에 대해 한층 더 깊이 이해할 수 있었다.

이전에는 useCallback이나 useMemo 같은 훅들이 내부에서 어떻게 동작하하며 어떤 식으로 성능에 영향을 미치는지 명확하게 이해하지 못했지만, 이번 과제를 통해 그 동작 원리와 실제 적용 효과를 직접 체감할 수 있었다.

특히 인상 깊었던 점은 useCallback이 결국 useMemo의 props로 함수가 들어간 것과 동일한 형태라는 사실을 이제야 명확히 인지하게 된 것이다. 이전에도 자주 사용해왔지만, 이 둘의 구조적 유사성을 별생각 없이 지나쳤다는 점에서 나 스스로도 무관심했음을 느꼈다. 동시에 무의식적으로 사용했던 기술에 대해 더 깊이 이해한 느낌이기도 하다.

이러한 이해가 생기고 나니, React 내부에서 어느 정도 최적화를 자동으로 처리해준다는 점도 납득이 되었고, 덕분에 최적화 적용이 훨씬 수월해졌다.

초반에는 useCallback을 사용하면 무조건 함수가 최적화된다고 막연히 생각했지만, 실제로는 의존성 배열이 변경되지 않을 때에만 동일한 참조를 유지하며 최적화가 일어난다는 점을 직접 확인했다. 이 과정을 통해, props로 함수가 전달될 때 참조 고정이 얼마나 중요한지를 체감할 수 있었고, 자연스럽게 리렌더링 최적화의 핵심 개념들을 실무 관점에서 이해하게 되었다.

무엇보다 중요한 건, 단순히 기술을 사용하는 것보다 “왜 이 상황에서 이 훅을 써야 하는가?”를 스스로 설명할 수 있는 것인 것 같다.

트러블 슈팅

실패한 테스트: "로그인/로그아웃 시 Header, ComplexForm, NotificationSystem만 리렌더링되어야 한다."

이번 과제에서 가장 오랫동안 붙잡았던 문제다.

✅ 문제 상황

초기 구조는 모든 상태 관리가 App 컴포넌트에 모여 있었다. 로그인/로그아웃 관련 함수(login, logout)도 App 내부에서 선언되어 props로 내려졌고, 알림(Notification)도 별도로 addNotification() 함수를 props 혹은 context로 내려서 사용하고 있었다.

// App.tsx 내부
const login = () => {
  setUser(...);
  addNotification(...); // 직접 호출
};

그런데 Header에서 login()을 호출했을 때 Notification이 뜨지 않는 문제가 있었고, 이 때문에 ComplexForm, NotificationSystem이 리렌더링되지 않아서 테스트가 실패하는 상황이 반복되었다.

✅ 원인 분석

// Header.tsx 내부
const handleLogin = () => {
  login("user@example.com", "password");
  // ❌ addNotification()을 여기서 직접 호출해야 함
};

이전 구조에서는 Header가 login()만 호출해도 user는 바뀌었지만, 알림(Notification)은 따로 처리하지 않으면 발생하지 않았다.
즉, NotificationContext의 상태가 변하지 않아서, 이 컨텍스트를 구독하고 있는 ComplexForm, NotificationSystem도 리렌더링되지 읺있다.

해결 방법: AuthProvider 내부로 알림 로직 통합

해결의 핵심은 AuthProvider 안에서 NotificationContext를 직접 사용하도록 구조를 바꾼 것이었다.

✅ 기존 구조

App.tsx에서 login, logout, addNotification을 따로 선언하고 각각 내려줌

✅ 변경 후 구조

AuthProvider 내부에서 NotificationContext를 사용하고, login()과 logout()이 알림도 함께 발생시킴

//  변경된 AuthProvider.tsx
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const { addNotification } = useNotificationContext(); // 내부에서 바로 사용
  const [user, setUser] = useState<User | null>(null);

  const login = useCallback((email: string) => {
    setUser({ id: 1, name: "홍길동", email });
    addNotification("성공적으로 로그인되었습니다", "success"); 
  }, [addNotification]);

  const logout = useCallback(() => {
    setUser(null);
    addNotification("로그아웃되었습니다", "info"); 
  }, [addNotification]);

  const value = useMemo(() => ({ user, login, logout }), [user, login, logout]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

Context 구조를 어떻게 나누고, 그 안에서 책임을 어떻게 위임할지에 따라 리렌더링 성능 최적화 여부가 달라진다는 걸 확인할 수 있었다.

상태 변화의 의미 있는 발생 위치가 어디인지, 그리고 그 변화가 필요한 컴포넌트에만 영향을 주는지까지 고려하는 게 진짜 최적화라는 걸 실감했다.

코드 품질 개선

기능 단위 폴더 분리

  • Context 기능별 폴더 분리 (features/auth, features/notification 등) 기능별 도메인으로 폴더 구조 정리
  • 각 기능은 자체적으로 Context, Provider, hooks, UI 컴포넌트를 관리하여 응집도 강화

상태의 범위 고민

  • notification, auth는 전역 관리
  • form, items는 불필요한 전역화 없이 기능에 맞게 로컬 상태로 유지
  • context를 무조건 global로 쓰지 않아도 될 것 같다는 생각을 했다. (특정 도메인 성격이 두드러지는 context라면,,? )

상태관리 리팩토링 (custom hooks)

  • ItemList, ComplexForm 등의 로직을 useItems, useComplexForm 커스텀 훅으로 분리
  • input 핸들러, 계산 로직, 상태 변경 등도 해당 훅 내부로 옮겨 UI와 로직을 명확히 분리

ItemList의 상태는 원래 App에서 내려받고 있었지만, 이보다는 독립적인 훅인 useItems로 분리하는 방식이 더 자연스럽게 느껴졌다. 필터, 가격 계산 등의 로직도 이 훅으로 이동시켜 UI와 로직을 깔끔하게 나눌 수 있었다.

✅ 이전 - App에서 ItemList를 받아서 props로 전달받는 구조

// App.tsx
const App = () => {
  const baseItems = useMemo(() => generateItems(1000), []);
  const [extraItems, setExtraItems] = useState<Item[]>([]);
  const allItems = useMemo(() => [...baseItems, ...extraItems], [baseItems, extraItems]);

  const addItems = () => {
    setExtraItems((prev) => [
      ...prev,
      ...generateItems(1000, baseItems.length + prev.length)
    ]);
  };

  return (
    <ItemList items={allItems} onAddItemsClick={addItems} />
  );
};

// ItemList.tsx
const ItemList = ({ items, onAddItemsClick }) => {
  // items, onAddItemsClick을 props로 사용
};

✅ 이후 - ItemList를 관리하는 useItemList hook을 분리하고 여기서 필요한 상태값들을 전달받도록 변경

// ItemList.tsx
import { useItems } from "./useItemList";

const ItemList = () => {
  const {
    addItems,
// 등등... 
  } = useItemList();

  return (
    <>
      <button onClick={addItems}>추가</button>
      {/* 나머지 UI */}
    </>
  );
};

학습 효과 분석

이번 프로젝트를 통해 리액트의 렌더링 흐름을 실제로 제어하는 경험을 했다.

  • useMemo는 값의 재계산을 막기 위한 캐싱 전략

  • useCallback은 함수 참조의 일관성을 위한 메모이제이션

  • React.memo는 props가 변경되지 않으면 리렌더링하지 않도록 하는 최종 방어선

이 원리들을 정확히 이해한 상태에서 직접 구조를 리팩토링할 수 있어 좋았고, *"Context는 작게 쪼개고, 관심사는 명확히 분리해야 한다"*는 아키텍처적 인사이트도 얻을 수 있었다.

테스트 기반 개발의 중요성

사실 테스트가 없었다면 ItemList가 은근슬쩍 리렌더링되고 있다는 사실을 몰랐을 수도 있었다. 테스트를 기반으로 성능 이슈를 추적하고, 결과적으로 구조까지 수정하게 된 경험은 TDD의 좋은 예시였다고 생각한다.

리뷰 받고 싶은 부분

  • React Context의 관심사 분리와 성능 최적화 구조가 잘 설계된 것인지
    • Context 구조와 value 분리 방식이 적절한지 (특히 중첩 구조, 관심사 분리 관점에서)
  • Context가 많아졌을 때 Provider 감싸는 순서나 위치에 대한 좋은 패턴이 있는지
  • 구조 설계(폴더 구조, 훅 분리 등)가 테스트하기 쉽게 설계되었는지에 대한 피드백도 받고 싶어요.
  • 추가로 리렌더링 테스트 실제로 현업에서 자주 하면서 작업하시는지? 궁금합니다!

@Jeong-wonho
Copy link

크으 멋집니다..!

Copy link

@ywkim95 ywkim95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

효정님 고생 많으셨습니다!
코드를 구현하는 과정에서 여러가지 시도를 하신게 보입니다!
이후에는 코드의 일관성이나 단일 책임 원칙 등을 고려하셔서 구현하시면 더 좋은 코드가 나올 듯합니다!
이번 주도 고생 많으셨고, 다음 주도 화이팅입니다!

return objA.every((x, i) => deepEquals(x, objB[i]));
}

const keys = new Set([...Object.keys(objA), ...Object.keys(objB)]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array가 아니고 Object인 경우에서도 objAobjB의 키의 갯수를 비교하여 즉시 반환을 추가로 구현하셔도 좋았을 듯합니다!
Set으로 구현하신 이유가 있을까요? Set이 아니라 키의 갯수를 비교 후 같으면 하나의 Object.keys 로 루프를 돌리는 방식은 어떻게 생각하시나요?


const keys = new Set([...Object.keys(objA), ...Object.keys(objB)]);

for (const key of keys) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every로 구현하시는 것에 대해서도 고려해보시면 좋을 듯 합니다!

Comment on lines +12 to +22
const isSame = prevPropsRef.current && _equals(prevPropsRef.current, props);

if (isSame && renderedRef.current) {
return renderedRef.current;
}

const rendered = createElement(Component, props);
renderedRef.current = rendered;
prevPropsRef.current = props;

return rendered;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 생각하던 방식과 반대로 구현하셨군요!
저는 이렇게 구현하는 게 코드의 양이 조금 늘어나더라도 가독성이 좋다고 느껴지네요!

Comment on lines +9 to +11
const deps = [..._deps];

return useMemo(() => factory, deps);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 하나만 사용하는 변수라면 따로 할당하지 않고, 바로 사용하는 건 어떻게 생각하세요?

return useMemo(() => factory, [..._deps]);

Comment on lines +13 to +24
const login = useCallback(
(email: string) => {
setUser({ id: 1, name: "홍길동", email });
addNotification("성공적으로 로그인되었습니다", "success");
},
[addNotification],
);

const logout = useCallback(() => {
setUser(null);
addNotification("로그아웃되었습니다", "info");
}, [addNotification]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

login 과 logout에 대해서 addNotification을 deps로 두신 이유가 어떤 걸까요? 궁금합니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants