React, Beyond the Basics 17일차

anvel·2025년 4월 7일

항해 플러스

목록 보기
10/39
post-thumbnail

3주차 과제 완료

이번주 과제는 이미 React에 구현되어있는 훅들을 직접 구현해보는 과제였습니다.
먼저 얕은 비교(shallowEquals)와 깊은 비교(deepEquals)를 하는 함수를 만들고,
useRef, useCallback, useMemo Hooks를 구현하였습니다.
그리고 이를 활용하여, 컴포넌트 랜더링을 최적화하고, context를 분리하는 과제였습니다.

1. 얕은 비교 vs 깊은 비교

  • 얕은 비교(Shallow Equality)

    • 1단계 깊이 까지만 비교하고,
    • 객체의 참조값, 또는 1차 속성값만 비교합니다.
    export const shallowEquals = <T>(objA: T, objB: T): boolean => {
      // 타입이 다른 지 먼처 확인
      if (typeof objA !== typeof objB) return false;
    
      // 배열인 경우
      if (Array.isArray(objA) && Array.isArray(objB)) {
        if (objA.length !== objB.length) return false;
        return objA.every((a, idx) => a === objB[idx]);
      }
    
      // 객체일 경우
      if (isObject(objA) && isObject(objB)) {
        const keysA = Object.keys(objA);
        const keysB = Object.keys(objB);
        if (keysA.length !== keysB.length) return false;
        for (const key of keysA) {
          if (objA[key as keyof T] !== objB[key as keyof T]) {
            return false;
          }
        }
        return true;
      }
    
      // 나머지 원시 타입
      return objA === objB;
    };
    
    const isObject = (
      value: unknown,
    ): value is Record<string | number | symbol, unknown> => {
      return typeof value === "object" && value !== null && !Array.isArray(value);
    };
    
  • 깊은 비교(Deep Equality)

    • 객체의 하위 속성까지 재귀적으로 비교하며,
    • 구조와 값이 완전히 같아야 같은 값이라고 판단합니다.
    export const deepEquals = <T>(objA: T, objB: T): boolean => {
      // 타입이 다르면 무조건 false
      if (typeof objA !== typeof objB) return false;
    
      // 배열일 경우
      if (Array.isArray(objA) && Array.isArray(objB)) {
        if (objA.length !== objB.length) return false;
        return objA.every((item, i) => deepEquals(item, objB[i]));
      }
    
      // 객체일 경우
      if (isObject(objA) && isObject(objB)) {
        const keysA = Object.keys(objA);
        const keysB = Object.keys(objB);
        if (keysA.length !== keysB.length) return false;
        return keysA.every((key) =>
          deepEquals(objA[key as keyof T], objB[key as keyof T]),
        );
      }
    
      // 나머지 원시 타입
      return objA === objB;
    };
    
    const isObject = (
      value: unknown,
    ): value is Record<string | number | symbol, unknown> => {
      return typeof value === "object" && value !== null && !Array.isArray(value);
    };

2. Hooks

React의 함수형 컴포넌트에서 상태관리나 생명주기에 따른 동작을 수행하기 위한 API입니다. 클래스 컴포넌트에서만 제공되던 기능이, Hooks의 도입으로 함수형 컴포넌트에서 선언적으로 사용할 수 있게 되었고, 재사용성이 확대되어 커스텀 훅들을 이용한 중복 로직의 제거가 가능해졌습니다.

  • useRef

    • 랜더링의 영향을 주지 않고, 값을 유지하는 mutable value 저장소,
    • DOM 접근이나 랜더링 사이의 값을 기억할 때 사용합니다.
    
    import { useState } from "react";
    
    export function useRef<T>(initialValue: T): { current: T } {
      const [customRef] = useState({ current: initialValue });
      return customRef;
    }
    
  • useMemo

    • 복잡한 계산 결과를 의존성 배열 기준으로 캐싱하여 불필요한 재계산을 방지합니다.
    
    import { DependencyList } from "react";
    import { shallowEquals } from "../equalities";
    import { useRef } from "./useRef";
    
    export function useMemo<T>(
      factory: () => T,
      _deps: DependencyList,
      _equals = shallowEquals,
    ): T {
      const memoRef = useRef<{ value: T; deps: DependencyList } | null>(null);
      if (memoRef.current === null || !_equals(memoRef.current.deps, _deps)) {
        memoRef.current = { value: factory(), deps: _deps };
      }
      return memoRef.current.value;
    }
    
  • useCallback

    • 특정 함수가 불필요하게 재생성되는 것을 방지하며,
    • 의존성 배열에 따라 변화가 없다면 이전 함수 참조를 재사용합니다.
    
    import { DependencyList } from "react";
    import { useMemo } from "./useMemo";
    
    export function useCallback<T extends Function>(
      factory: T,
      _deps: DependencyList,
    ) {
      return useMemo(() => factory, _deps);
    }

3. Context

컴포넌트 트리 안에서 하위 컴포넌트 까지 전역 상태의 값, 함수 등을 공유할 수 있게 하는 기능으로, 커스텀 훅으로 접근하여 사용하면 하위 컴포넌트 까지의 Props Drilling 회피가 가능해집니다.

  • 흐름

    • Context 객체 생성
      const ThemeContext = createContext({theme:"light", toggleTheme: () => {} });
    • Provider에 값 주입 및 하위 컴포넌트 배치
      <ThemeContext.Provider value={{ theme, toggleTheme }}>
        <App />
      </ThemeContext.Provider>
    • use- 커스텀 훅을 통한 하위 컴포넌트에서의 접근
      const { theme, toggleTheme } = useContext(ThemeContext);
  • ThemeContext.tsx

    
    import React, { createContext, useContext, useState, ReactNode } from "react";  
    type Theme = "light" | "dark";
    
    interface ThemeContextType {
      theme: Theme;
      toggleTheme: () => void;
    }  
    const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
    
    export const ThemeProvider = ({ children }: { children: ReactNode }) => {
      const [theme, setTheme] = useState<Theme>("light");
    
      const toggleTheme = () => {
        setTheme((prev) => (prev === "light" ? "dark" : "light"));
      };
    
      const value = { theme, toggleTheme };
    
      return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
    };
    
    export const useThemeContext = () => {
      const context = useContext(ThemeContext);
      if (!context) {
        throw new Error("useThemeContext must be used within a ThemeProvider");
      }
      return context;
    };
    

0개의 댓글