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

[9팀 박지수] [Chapter 1-3] React, Beyond the Basics #7

Closed
wants to merge 1 commit into from

Conversation

keyonnaise
Copy link

@keyonnaise keyonnaise commented Dec 29, 2024

과제 체크포인트

기본과제

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

심화 과제

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

과제 셀프회고

이번 주 과제는 지난 1, 2주차에 비해 비교적 수월하게 진행할 수 있었다. 본인 같은 경우 심화 과제보다는 기본 과제를 진행하며 더 많은 고민과 생각을 하게 되었는데, 이는 기본기가 부족한 탓일지도 모르겠다. 하지만 그동안 제공되는 기능을 문서만 보고 별 생각 없이 사용했던 것과 달리, 내부 원리를 어렴풋이나마 이해하게 되면서 재미를 느낄 수 있었다.

기술적 성장

처음 useRef를 직접 구현할 때, 내부에 useEffect를 사용하면서 "타이밍 이슈가 발생하지 않을까?"라는 고민에 학습 메이트(최기환 님)의 조언을 구하게 됐는데 그냥 아무것도 사용하지 않고 useRef만으로 구현하는게 좋겠다는 조언을 얻을 수 있었다. 결과, 고민했던 부분을 깔끔하게 해결할 수 있었는 이를 통해 리액트 생명주기에 대한 이해가 아직 부족하다는 것을 깨닫는 좋은 기회가 되었다.

코드 품질

이번 과제는 "메모이제이션", "리렌더링 최소화"에 초점이 맞춰져 있는 것 같아 그 부분에 신경을 써서 과제를 진행했다. 다만 언젠가 메모이제이션에 대해 상반되는 두 가지 의견을 접한 적이 있었는데 다음과 같았다.

잘 모르겠으면 모든 곳에 useCallback, useMemo, memo를 사용해라. 어차피 메모이제이션 하는데 자원 많이 안 들어간다.

어쨌건 자원이 들어가는 건 맞는데 메모이제이션은 조상님이 해주냐? 잘 구분해서 써야 한다.

두 의견 모두 일리가 있는 것 같았지만, 과제를 진행하는 동안에는 구분해서 사용하는 것이 본인의 성장에 도움이 될 것이라고 판단하여 다음과 같은 기준을 적용했다.

  • useMemo : 값의 업데이트가 빈번하지 않을 것으로 예상되는 경우에 사용
  • useCallback : 예전에 접했던 토스의 usePreservedCallback과 Radix UI의 useCallbackRef라는 커스텀 훅을 떠올리며 usePreservedCallback이라는 커스텀 훅을 만들어 사용했다. 이 훅은 함수의 참조 값은 유지하면서 의존성 배열 없이도 함수 내부에서 사용하는 상태 값이 업데이트될 때 그 값을 반영할 수 있도록 한다. 주로 onClick이나 onChange 등의 이벤트 함수에 사용했다. 다만 usePreservedCallback은 참조 값이 변하지 않기 때문에 함수의 업데이트를 감지해야 하는 상황에서는 useCallback을 사용해야 되겠다.
// usePreservedCallback과 useCallback 예시 코드
const [count, setCount] = useState(0)

const preservedFn = usePreservedCallback(() => console.log(count))

const callbackFn = useCallback(()=> console.log(count), [count])

const increase = () => setCount(prev => prev++)

useEffect(()=>{
  console.log("실행되지 않아용!!");
}, [preservedFn])

useEffect(()=>{
  console.log("실행 돼용!!");
}, [callbackFn])
  • memo : 사용하지 않아도 과제 통과에는 문제가 없었다. 그래도 Portal 컴포넌트에 달아주어 외부와 격리시키는 느낌으로 진행해 봤다.

그래서 기준을 바탕으로 다음 코드와 같이 값이 수시로 바뀌는 경우에는 메모이제이션을 하지 않았다.

// ItemList.tsx

function ItemList(){
  // 생략...
  // 타이핑 할 때마다 `filter` 값이 변화하므로 메모이제이션 할 이유가 없을 것 같았음
  const filteredItems = items.filter(
    (item) =>
      item.name.toLowerCase().includes(filter.toLowerCase()) ||
      item.category.toLowerCase().includes(filter.toLowerCase()),
  );

  const totalPrice = filteredItems.reduce((sum, item) => sum + item.price, 0);
  const averagePrice = Math.round(totalPrice / filteredItems.length) || 0;
  // 생략...
}

Context API 사용에 있어서도 여러 가지 개선을 시도했다. 먼저 Radix UI의 createSafeContext() 함수를 참고하여 undefined safe한? 컨텍스트 생성 함수를 구현했다.

// createSafeContext.ts

function createSafeContext<ContextValue extends object | null>(
  rootComponentName: string,
  defaultValue?: ContextValue,
){
  // 생략...
}

이를 통해서 안전 컨텍스트의 생성이 가능하도록 했다. 다음으로 컨텍스트를 만들어볼 차례인데 이전에는 상태(state)와 액션 함수(action functions)를 하나의 컨텍스트에서 관리하여 불필요한 리렌더링이 발생하는 문제가 있었다.

// AS-IS (상태와 액션을 하나의 컨텍스트에서 관리)

interface ContextState {
 // 생략...
}

interface ContextActions {
 // 생략...
}

type ContextType = ContextState & ContextActions

const [SafeContextProvider, useContext] = createSafeContext<ContextType>("ContextProvider")

fucntion Provider(){
  const [state, setState] = useState<ContextState>({
    // 생략...
  })
  const actions = useMemo<ContextActions>(() => ({
    // 생략...
  }), [])
  const value = useMemo<ContextState & ContextActions>(() => ({
    ...state,
    ...actions,
  }), [state, actions])

  return (
    <SafeContextProvider {...value}></SafeContextProvider>
  )
}

위와 같이 하나의 컨텍스트를 사용할 경우, 액션 함수만 사용하는 컴포넌트에서도 상태 값의 업데이트가 발생할 때 리렌더링이 발생했다. 따라서 이번 과제에서는 상태 컨텍스트와 액션 컨텍스트를 분리하여 사용했다.

// TO-BE (상태와 액션을 분리된 컨텍스트에서 관리)

interface ContextState {
 // 생략...
}

interface ContextActions {
 // 생략...
}

const [StateProvider, useStateContext] = createSafeContext<ContextType>("ContextProvider")
const [ActionsProvider, useActionsContext] = createSafeContext<ContextType>("ContextProvider")

function Provider(){
  const [state, setState] = useState<ContextState>({
    // 생략...
  })
  const actions = useMemo<ContextActions>(() => ({
    // 생략...
  }), [])

  return (
    <StateProvider {...state}>
      <ActionsProvider {...actions}></ActionsProvider>
    </StateProvider>
  )
}

상태와 액션 컨텍스트를 분리하여 사용한 결과, 상태 값이 변경되더라도 액션 함수만 사용하는 컴포넌트들은 리렌더링되지 않는 것을 확인했다.

학습 효과 분석

과제 피드백

본인만 그렇게 느끼는 것일 지도 모르겠지만 이번 주차 과제는 지난 1, 2주차 과제보다 쉽게 느껴졌다.
좀 더 난이도 올려도 괜찮을 듯?

다만 과제 진행 중 ComplexForm 컴포넌트에서 약간의 문제가 발생했었는데, 본인 생각에 "요번 심화 과제는 리렌더링에 초점이 맞춰져 있고, ComplexForm 컴포넌트는 user의 값과 notifications 값을 내부에서 사용하지 않으니 그럼 두 값이 변경되더라도 리렌더링을 하면 안되겠네?!"라고 안일하게 판단하며 아래의 AS-IS 코드와 같이 코드를 작성 했더니 테스트를 통과하지 못했다.

// AS-IS

function ComplexForm() {
  renderLog("ComplexForm rendered");

  // ...생략
}

이유를 찾고자 테스트 코드를 까보니 user 값과 notification 값의 변경이 있을 때도 ComplexForm 컴포넌트는 리렌더링 되어야 한다더라... 처음엔 그 사실도 모르고 엄청 헤맸는데 이에 대한 해결 방안으로, 아래와 같이 코드를 추가하여 테스트를 통과할 수 있었다.

// TO-BE

function ComplexForm() {
  renderLog("ComplexForm rendered");

  // NOTE: 테스트 통과를 위한 코드
  useUserStateContext("ComplexForm");
  useNotificationSystemStateContext("ComplexForm");

  // ...생략
}

리뷰 받고 싶은 내용

  1. useMemo나 useCallback 등 잘 사용하는 방법을 검색을 해봐도 알아서 적절하게 사용하라거나 그냥 다 쓰라는 내용 블라블라 코치님 팁? 취향 듣고 싶다 블라블라 지금은 앵간하면 쓰고 값이 수시로 바뀌어서 메모이제이션 하는 의미가 없을 것 같으면 안 함
  2. Provider를 중첩해서 쓰면 마치 Callback Hell 처럼 모양과 가독성이 심각해지는데 블라블라
  3. 타입 정의 어디에? 오픈소스 라이브러리들 보면 파일명.types.ts 라는 파일에 관리하는 곳도 있고 단일 파일로 소스 코드와 함께 작성하는 곳도 있던데 기준이 궁금. 개인 취향인가?

작성 중···

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.

코드 너무너무 잘 봤습니다!
지수님 코드를 보니 모르는 부분이 많아서 코드 리뷰를 하면서 잘 배워간다는 느낌을 많이 받았습니다!
이번 주차도, 이후 주차도 화이팅입니다!

@@ -27,6 +27,12 @@ export default tseslint.config(
],
},
},
{
Copy link

Choose a reason for hiding this comment

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

eslint의 룰을 직접 작성해보셨군요!
좋은 인사이트 얻어갑니다!

Copy link

Choose a reason for hiding this comment

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

포탈로 구현하셨네요!
저는 여기서 포탈로 구현하신 분 지수님이 처음인 것 같아요!
멋지십니다!
참고로 말씀드리면 나중에 index.html을 수정하시지 않고 직접 추가하는 방식도 있답니다...! (이미 알고계신 것 같긴합니다 ㅎㅎ)

Comment on lines +47 to +54
interface ContextState {
group: Map<number, NotificationType>;
}

interface ContextActions {
addNotification(notification: NotificationType): void;
removeNotification(id: number): void;
}
Copy link

Choose a reason for hiding this comment

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

State & Action으로 나누시다니 코드 정말 잘짜시네요!
또 한 번 배우고 갑니다!

export const [
NotificationSystemStateProvider,
useNotificationSystemStateContext,
] = createSafeContext<ContextState>("NotificationSystemProvider");
Copy link

Choose a reason for hiding this comment

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

createSafeContext는 처음보네요! createContext와는 어떤 차이점이 있나요!?

import { isPlainObject, isPrimitive } from "../utils";

export function deepEquals<T>(first: T, second: T): boolean {
if (isPrimitive(first) || isPrimitive(second)) {
Copy link

Choose a reason for hiding this comment

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

비교 함수를 따로 빼셨네요! 저는 과제 통과를 우선으로 생각해서 구현하지 않았는데 한 수 배웠습니다!

export function useMemo<T>(
factory: () => T,
_deps: DependencyList,
_equals = shallowEquals,
): T {
// 직접 작성한 useRef를 통해서 만들어보세요.
return factory();
const memoized = useRef<{ value: T; deps: DependencyList }>();
Copy link

Choose a reason for hiding this comment

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

하나의 ref로 작성하셨군요..! 저희 7,10팀원분들도 전부 하나로 작성하셨더라구요.. 저만 두개로 작성했나봐요! 하나로 구현하는 것도 연습해봐야겠습니다!

// React의 useState를 이용해서 만들어보세요.
return { current: initialValue };
return useState(() => ({ current: initialValue }))[0];
Copy link

Choose a reason for hiding this comment

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

변수를 만들지 않는 깔끔함..!
좋은 인사이트 감사드려요!

아 그리고 위의 useRef를 여러 번 선언하신 이유가 있나요? react의 코드에서는 여러 개의 타입을 받기 위해서 선언한 것은 보았는데 여기에서도 구현하신 이유가 궁금합니다!

@@ -0,0 +1,31 @@
import { createContext, useContext, useMemo } from "react";

export function createSafeContext<ContextValue extends object | null>(
Copy link

Choose a reason for hiding this comment

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

위에서의 궁금하던 createSafeContext가 여기있었네요!
저는 다른 함수가 있는 줄 알았는데 직접 구현하신 것이었군요!
대단하십니다..!
Provider까지 내부에서 구현하신 것을 보니 굉장히 깔끔하게 코드가 짜여져 있다고 생각이 듭니다!
다음 번에는 내부의 Provider와 useSafeContext도 분리해서 구성해보는 것도 괜찮다고 생각됩니다!
좋은 인사이트 감사드려요!

@@ -0,0 +1,5 @@
export function isNil<T>(
Copy link

Choose a reason for hiding this comment

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

nil 이라면... 다른 언어를 배워보셨군요...!
어떤 걸 배우셨나요?

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.

2 participants