이번주 과제는 이미 React에 구현되어있는 훅들을 직접 구현해보는 과제였습니다.
먼저 얕은 비교(shallowEquals)와 깊은 비교(deepEquals)를 하는 함수를 만들고,
useRef, useCallback, useMemo Hooks를 구현하였습니다.
그리고 이를 활용하여, 컴포넌트 랜더링을 최적화하고, context를 분리하는 과제였습니다.
얕은 비교(Shallow Equality)
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);
};
React의 함수형 컴포넌트에서 상태관리나 생명주기에 따른 동작을 수행하기 위한 API입니다. 클래스 컴포넌트에서만 제공되던 기능이, Hooks의 도입으로 함수형 컴포넌트에서 선언적으로 사용할 수 있게 되었고, 재사용성이 확대되어 커스텀 훅들을 이용한 중복 로직의 제거가 가능해졌습니다.
useRef
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);
}
컴포넌트 트리 안에서 하위 컴포넌트 까지 전역 상태의 값, 함수 등을 공유할 수 있게 하는 기능으로, 커스텀 훅으로 접근하여 사용하면 하위 컴포넌트 까지의 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;
};