Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7팀 김주광] [Chapter 1-3] React, Beyond the Basics #9

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

Dayglow0926
Copy link

@Dayglow0926 Dayglow0926 commented Dec 29, 2024

과제 체크포인트

기본과제

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

심화 과제

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

과제 셀프회고

기술적 성장

  • 새로 학습한 개념

    • 얕은 비교와 깊은 비교는 이론으로만 이해하고 해당 기능의 개념을 구현하는 것은 새로운 경험이였습니다.
  • 기존 지식의 재발견/심화

    • useRef, useMemo, useCallback를 구현해보면서 대략적으로만 알고 있던 지식을 구현해봄으로 써 더 자세하게 학습할 수 있었습니다.
  • 구현 과정에서의 기술적 도전과 해결

    • 사실 모든 기능이 도전과 해결의 연속이였습니다.
    • 얕은 비교와 깊은 비교는 노션에 제시해주신 주석을 단서로 하나씩 구현해보고 제가 바꿔보면서 어떠한 차이가 있는지 알아보는 과정이였습니다.
    • useRef 의 경우 useState 와 한줄로 작성이 가능하다는 힌트를 듣고 제가 알고있는 지식으로 구현해보았지만 제가 알고 있는 useRef 를 구현하는 데 실패했지만 여러 도움을 통해 useRef 를 구현하는데 성공하며 동시에 useState를 이런식으로 활용할 수 있다는 것을 처음 알게되었습니다.

코드 품질

  • 특히 만족스러운 구현

    • 첫번째는 useRef 입니다.
      해당 코드는 저에게 두가지의 유용한 지식을 알게 해주었습니다. 하나는 useState 의 활용방법이고 다른 하나는 useState 초기값의 리렌더링 문제와 해결방법을 알게 해주었다는 점에서 저에게는 뜻 깊은 코드였습니다.

    • 두번째는 useMemo 입니다.
      해당 코드 하나를 구현함으로써 만들수 있는 useCallback, useDeepMemo 이 두 기능을 모두 동시에 만들 수 있다는 것에 여러가지를 배우게 되었습니다.

  • 리팩토링이 필요한 부분

    • App.js 파일은 리팩토링이 필요하다고 생각이되어 context, provider, custom hook 을 분리하여 작업을 진행했습니다.
    • 그래도 부족하다고 느끼는 점은 App 에 남아있는 provider 의 콜백지옥은 어떻게 해결할 수 있을까의 의문이 남아있습니다.

학습 효과 분석

  • 가장 큰 배움이 있었던 부분
    • 첫번째는 hook 구현을 함으로써 구현된 hook을 이해할 수 있었습니다.
      ( useMemo, useCallback, useRef 등등 )
    • 두번째는 hook 을 활용함으로써 단순 이해뿐만 아니라 조금 더 깊은 이해를 체득할 수 있었던 것 같습니다.
      ( useState, useMemo, useCallback, useRef 등등 )
    • 세번째는 팀원분들의 pr 내용을 통해 제가 미쳐 생각하지 못한 부분을 배울 수 있었습니다.
      ( useState 의 활용방법, Pick 의 사용방법 )

과제 피드백

  • 과제에서 모호하거나 애매했던 부분

    • 얕은 비교와 깊은 비교를 노션의 제시해주신 가이드를 따라 작성을 했지만 이후 제가 깊은 비교의 있는 내용을 얕은 비교에 넣고 재귀호출하는 부분만 수정하여 적용하니 테스트를 무사히 통과하였습니다.
      코치님께서 제시해주신 가이드와 달라서 사실 이게 맞게 구현한건지 잘 모르겠습니다.
  • 과제에서 좋았던 부분

    • hook 을 구현함으로써 hook 에 대한 이해도를 높혀주고 해당 hook 을 사용함으로써 제가 이해한 내용을 체감할 수 있었던 점이 좋았습니다.

리뷰 받고 싶은 내용

피드백을 부탁드리고 싶은 내용은

첫번째 얕은 비교와 깊은 비교를 제가 아래와 같이 구현하였습니다.

제시해주신 가이드

// shallowEquals 함수는 두 값의 얕은 비교를 수행합니다.
export function shallowEquals(objA: any, objB: any): boolean {
  // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우)
  // 2. 둘 중 하나라도 객체가 아닌 경우 처리
  // 3. 객체의 키 개수가 다른 경우 처리
  // 4. 모든 키에 대해 얕은 비교 수행

  // 이 부분을 적절히 수정하세요.
  return objA === objB;
}

// deepEquals 함수는 두 값의 깊은 비교를 수행합니다.
export function deepEquals(objA: any, objB: any): boolean {
  // 1. 기본 타입이거나 null인 경우 처리

  // 2. 둘 다 객체인 경우:
  //    - 배열인지 확인
  //    - 객체의 키 개수가 다른 경우 처리
  //    - 재귀적으로 각 속성에 대해 deepEquals 호출

  // 이 부분을 적절히 수정하세요.
  return objA === objB;
}

제가 작성한 코드

export function shallowEquals<T>(objA: T, objB: T): boolean {
  // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우)
  if (objA === objB) {
    return true;
  }

  if (
    Array.isArray(objA) &&
    Array.isArray(objB) &&
    objA.length === objB.length
  ) {
    return objA.every((v, i) => v === objB[i]);
  }

  // 4. 모든 키에 대해 얕은 비교
  if (isRecord(objA) && isRecord(objB)) {
    // 3. 키 개수 비교
    if (Object.keys(objA).length !== Object.keys(objB).length) {
      return false;
    }
    return Object.keys(objA).every((key) => objA[key] === objB[key]);
  }

  return false;
}

export function deepEquals(objA: unknown, objB: unknown): boolean {
  if (objA === objB) {
    return true;
  }
  // 2. 둘 다 객체인 경우:
  //    - 배열인지 확인
  //    - 배열의 길이가 같은지 확인
  if (
    Array.isArray(objA) &&
    Array.isArray(objB) &&
    objA.length === objB.length
  ) {
    return objA.every((v, i) => deepEquals(v, objB[i]));
  }

  //    - 객체의 키 개수가 다른 경우 처리
  //    - 객체 null 인지 확인
  if (isRecord(objA) && isRecord(objB)) {
    // 3. 키 개수 비교
    if (Object.keys(objA).length !== Object.keys(objB).length) {
      return false;
    }
    return Object.keys(objA).every((key) => deepEquals(objA[key], objB[key]));
  }

  return false;
}

해당 코드는 노션에서 제시해주신 가이드와 조금 다르게 구현이 되었습니다.
해당 코드가 테스트는 통과하였지만 코드가 코치님의 의도하신 대로 작성되었는지가 궁금합니다.
문제가 있다면 어떤 부분에서 문제가 발생할 수 있는지 알고 싶습니다.

그리고 두번째는 App.js 의 콜백 지옥에 대한 해결방법이 있는지 알고 싶습니다.

// 메인 App 컴포넌트
const App: React.FC = () => {
  return (
    <ThemeProvider>
      <NotificationProvider>
        <LoginProvider>
          <Layout>
            <div className="container mx-auto px-4 py-8">
              <div className="flex flex-col md:flex-row">
                <div className="w-full md:w-1/2 md:pr-4">
                  <ItemList />
                </div>
                <div className="w-full md:w-1/2 md:pl-4">
                  <ComplexForm />
                </div>
              </div>
            </div>
          </Layout>
        </LoginProvider>
      </NotificationProvider>
    </ThemeProvider>
  );
};

위 코드는 제가 리팩토링을 진행하여 작성된 App.js 코드입니다.
보시다시피 Provider가 많아 짐으로써 depth 가 깊어지고 점차 가독성이 안좋아지고 있는 것 같습니다.
이러한 부분은 해결할 필요가 없는 건지 아니면 더 좋은 해결 방법이 있는 건지 궁금합니다.

세번째는 리팩토링 방법에 대해 궁금합니다.

image

제가 리팩토링을 진행하면서 위와 같은 폴더 구조를 만들어 분리를 진행했습니다.

[ context.ts ]

export const LoginContext = createContext<LoginContextType | undefined>(
  undefined,
);

[ provider.tsx ]

export const LoginProvider: React.FC<React.PropsWithChildren> = ({
  children,
}) => {
  const [user, setUser] = useState<User | null>(null);
  const { addNotification } = useNotificationContext();

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

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

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

  return (
    <LoginContext.Provider value={loginContextValue}>
      {children}
    </LoginContext.Provider>
  );
};

[ interface/user.ts ]

export interface User {
  id: number;
  name: string;
  email: string;
}

export interface LoginContextType {
  user: User | null;
  login: (email: string) => void;
  logout: () => void;
}

제가 이렇게 작성한 이유를 말씀드리자면,

context 폴더에서 context와 provider 를 나눈 이유는 사실 파일 이름을 정할 수 없어서 입니다.
context와 provider를 함께 넣은 파일 이름을 정할 수 없어서 나누었고 저는 그게 더 가독성이 좋다고 판단했습니다.

interface 의 경우는 공통된 주제를 기준으로 파일 하나를 통해 관리하는 것이 좋겠다고 생각이 들었습니다.
context 와 반대되는 이유이지만 context의 경우 context와 provider 두가지의 파일로만 나눠지지만
interface의 경우 각 interface 를 따로 파일로 관리하게된다면 너무 많은 파일을 생성할 가능성도 있고 주제를 기준으로 하나의 파일로 관리하는 편이 더 좋을 것 같다고 판단하였습니다.

저의 판단에서 어떠한 부분이 잘못됐는지 조언을 듣고 싶습니다.

감사합니다!

Comment on lines 13 to 19
if (
typeof objA === "object" &&
typeof objB === "object" &&
Object.keys(objA).length === Object.keys(objB).length
) {
return Object.keys(objA).every((key) => deepEquals(objA[key], objB[key]));
}

Choose a reason for hiding this comment

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

저는 객체 타입이랑 객체 길이 분기 처리를 따로 해놨는데 같이 처리하니 깔끔하네요! 저도 나중에 같이 처리 하도록 수정해야겠습니다ㅎㅎ

Choose a reason for hiding this comment

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

오.. 재귀 함수를 잘 이용하셔서 깔끔하게 작성하셨네요! 코드 잘보고 갑니다~

@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */
import { DependencyList } from "react";
import { useMemo } from "./useMemo";

export function useCallback<T extends Function>(
factory: T,
_deps: DependencyList,
) {
// 직접 작성한 useMemo를 통해서 만들어보세요.
Copy link

Choose a reason for hiding this comment

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

구현 잘 해주셔서 주석 지워주셔도 될 것 같아요!

- context 분리
- memo 추가
- useCallback 추가
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.

코드 너무 잘 봤습니다!
같이 성장해 나갈 수 있게 되어서 좋네요!
이번 주차도, 이후 남은 주차도 열심히 해봐요!

// 2. 둘 다 객체인 경우:
// - 배열인지 확인
if (Array.isArray(objA) && Array.isArray(objB)) {
return objA.every((v, i) => deepEquals(v, objB[i]));
Copy link

Choose a reason for hiding this comment

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

이부분에서도 길이가 같은지 체크하시면 더 안정적인 코드가 될 것 같아요!

Copy link

Choose a reason for hiding this comment

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

deepEquals와 코드의 구성이 조금 다른 걸로 보이는데 이렇게 구성하신 이유가 있을까요!
틀렸다라는 것보단 궁금해서 여쭤봅니다!

if (propsRef.current === null || !equals(props, propsRef.current)) {
propsRef.current = props;
// 4. props가 변경된 경우에만 새로운 렌더링 수행
componentRef.current = createElement(Component, props);
Copy link

Choose a reason for hiding this comment

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

저는 아예 컴포넌트를 반환하도록 <Component {...props} />로 했는데 저도 이 방법을 쓸 걸 그랬나봐요!
그러면 안전하게 ts로 반환이 되네요

export function useRef<T>(initialValue: T): { current: T } {
// React의 useState를 이용해서 만들어보세요.
return { current: initialValue };
const [ref] = useState<{ current: T }>({ current: initialValue });
Copy link

Choose a reason for hiding this comment

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

Suggested change
const [ref] = useState<{ current: T }>({ current: initialValue });
const [ref] = useState<{ current: T }>(() => ({ current: initialValue }));

찾아보니 이렇게 선언하는걸 추천하더라구요!
이유는 렌더링이 비싼 값이 선언되면 매번 리렌더가 발생하기 때문에 제안드린 것과 같이 함수로 구현하는 것을 추천드려요!

Comment on lines +13 to +18
const ref = useRef<{
deps: DependencyList;
value: T;
initialized: boolean;
}>({ deps: [], value: undefined as T, initialized: false });

Copy link

Choose a reason for hiding this comment

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

ref를 하나로 구현하셨군요! 좋은 인사이트를 주셔서 감사합니다!

src/App.tsx Outdated
@@ -23,11 +24,6 @@ interface Notification {

// AppContext 타입 정의
interface AppContextType {
Copy link

Choose a reason for hiding this comment

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

구현하신 방법이 가장 정석적인 방법이지만 저는 Pick 타입을 Nestjs의 dto를 구현할때 사용해본 적이 있어서 추천드립니다.
아래의 방법은 interface, type의 구성을 변경할 수 없을 때 유용해요. 기억해 두셨다가 필요하실 때 한 번 사용해보세요!

// 기존
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// 새로운 방법
const ThemeContext = createContext<Pick<AppContextType, "theme" | "toggleTheme"> | undefined>(undefined);

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.

5 participants