import React, { createContext, useState, useContext, useMemo } from 'react'; // useMemo 추가
// 1. Context 생성 (기본값 설정 가능)
interface UserContextType {
username: string | null;
login: (name: string) => void;
logout: () => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
// 2. Provider 컴포넌트 생성 (상태와 상태 변경 함수 제공)
export function UserProvider({ children }: { children: React.ReactNode }) {
const [username, setUsername] = useState<string | null>(null);
const login = (name: string) => setUsername(name);
const logout = () => setUsername(null);
// value 객체 메모이제이션 적용
const value = useMemo(() => ({ username, login, logout }), [username]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// 3. useContext 훅으로 Context 값 사용
export function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// 사용 예시 컴포넌트
function UserProfile() {
const { username, logout } = useUser();
return username ? (
<div>
<span>안녕하세요, {username}님!</span>
<button onClick={logout}>로그아웃</button>
</div>
) : (
<span>로그인이 필요합니다.</span>
);
}
// 앱 최상단 등에서 Provider로 감싸기
function App() {
return (
<UserProvider>
{/* ... 다른 컴포넌트들 ... */}
<UserProfile />
</UserProvider>
);
}
// 예시: 상태와 디스패치 함수 분리
const UserStateContext = createContext<string | null>(null);
const UserDispatchContext = createContext<{ login: (name: string) => void; logout: () => void } | undefined>(undefined);
// Provider에서 두 Context 제공
// 사용하는 컴포넌트는 필요한 Context만 구독
// function UserProfile() { const username = useContext(UserStateContext); ... }
// function LoginButton() { const { login } = useContext(UserDispatchContext); ... }
Q: "Context API의 성능 문제점과 최적화 방법은 무엇인가요?" A: "Provider의 value 변경 시 모든 Consumer의 불필요한 리렌더링이 발생할 수 있습니다. 최적화 방법으로는 value 메모이제이션(useMemo), Context 분리, 상태 읽기/변경 함수 분리 등이 있습니다."
// Redux Toolkit 예시 (Slice 정의)
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux'; // useSelector, useDispatch 추가
interface CounterState {
value: number;
}
const initialState: CounterState = { value: 0 };
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => { state.value += 1; }, // immer 내장으로 불변성 관리 용이
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => { // 액션 페이로드 타입 지정
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 스토어 설정
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
// RootState 타입 정의 (스토어 상태 전체 타입)
export type RootState = ReturnType<typeof store.getState>;
// AppDispatch 타입 정의 (디스패치 함수 타입)
export type AppDispatch = typeof store.dispatch;
// 컴포넌트에서 사용
function CounterComponent() {
// useSelector 사용 시 RootState 타입 지정
const count = useSelector((state: RootState) => state.counter.value);
// useDispatch 사용 시 AppDispatch 타입 사용 (선택적이지만 권장)
const dispatch: AppDispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>증가</button>
<button onClick={() => dispatch(decrement())}>감소</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5 증가</button>
</div>
);
}
shallow
함수 등으로 비교 로직 커스터마이징도 가능합니다.// Zustand 예시 (Store 생성)
import create from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
// 컴포넌트에서 사용
function CounterComponent() {
// 필요한 상태와 액션만 선택하여 구독 (렌더링 최적화)
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
// 또는 아래처럼 객체로 받아올 경우, count가 변하지 않아도 다른 상태 변경 시 리렌더링 될 수 있음 (shallow 옵션 고려)
// const { count, increment, decrement } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>증가</button>
<button onClick={decrement}>감소</button>
</div>
);
}
// Recoil 예시 (Atom 정의)
import React from 'react'; // React 임포트 추가
import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue } from 'recoil'; // RecoilRoot 추가
const counterState = atom({
key: 'counterState', // 고유한 ID
default: 0,
});
// 파생 상태 예시 (Selector)
const doubledCounterState = selector({
key: 'doubledCounterState',
get: ({ get }) => {
const count = get(counterState);
return count * 2;
},
});
// 컴포넌트에서 사용
function CounterComponent() {
const [count, setCount] = useRecoilState(counterState); // 상태 값과 setter 함수
const doubledCount = useRecoilValue(doubledCounterState); // 읽기 전용 값
return (
<div>
<p>Count: {count}</p>
<p>Doubled Count: {doubledCount}</p>
<button onClick={() => setCount(prev => prev + 1)}>증가</button> {/* 함수형 업데이트 권장 */}
<button onClick={() => setCount(prev => prev - 1)}>감소</button>
</div>
);
}
// 앱 최상단 등에서 RecoilRoot로 감싸기
function AppRecoil() {
return (
<RecoilRoot>
<CounterComponent />
</RecoilRoot>
);
}
Q: "여러 상태 관리 라이브러리(Redux, Zustand, Recoil 등)의 특징과 장단점을 비교 설명해주세요." A: 각 라이브러리의 핵심 철학, 장단점, 주요 사용 사례
라이브러리 | 특징 | 장점 | 단점 |
---|---|---|---|
Context API | React 내장 전역 상태 관리 도구 | 별도 라이브러리 설치 불필요 구조가 단순함 | 최적화 어려움 (불필요 리렌더링 발생 가능) 큰 프로젝트에선 비효율적 |
Redux Toolkit | Redux 공식 권장 툴킷 Predictable(예측 가능) | 강력한 DevTools 지원 미들웨어 및 비동기 처리에 강점 | 코드가 장황해질 수 있음 비교적 러닝커브 존재 |
Zustand | 간단하고 유연한 경량 상태 관리 Flux 패턴 불필요 | 사용법이 매우 직관적 필요한 부분만 리렌더링 가능 | 대규모 앱 아키텍처 설계시 주의 필요 DevTools 기능이 제한적 |
Recoil | Atom/Selector 단위 상태 관리 비동기 처리를 내장 지원 | 미세한 단위로 상태 분리 가능 비동기 상태 관리 편리 | 아직 완전한 안정성 확보는 아님 라이브러리 크기가 다소 있음 |
useSelector(state => state.user.name)
은 전체 상태 중 user.name
만 선택합니다.상태 저장소는 내부적으로 상태를트리(Tree)나해시 테이블(Hash Table/Map)형태로 관리할 수 있습니다. 구독자 목록은배열(Array)이나연결 리스트(Linked List)로 관리될 수 있습니다. 선택자의 메모이제이션은캐시(Cache)메커니즘을 활용합니다.
// 예시: API 호출 함수 (가정)
async function fetchTodos(status?: string) {
const url = status ? `/api/todos?status=${status}` : '/api/todos';
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
// React Query 예시
import { useQuery } from 'react-query'; // TanStack Query v4 기준, v5는 @tanstack/react-query
// 키: ['todos', { status: 'completed' }] -> 쿼리 함수에 객체로 전달됨
const { data, isLoading, error } = useQuery(
['todos', { status: 'completed' }],
() => fetchTodos('completed') // 쿼리 함수는 Promise를 반환해야 함
);
// SWR 예시
import useSWR from 'swr';
// fetcher 함수 정의 (SWR은 키를 fetcher의 첫 인자로 전달)
const fetcher = (url: string) => fetch(url).then(res => {
if (!res.ok) throw new Error('An error occurred while fetching the data.');
return res.json();
});
// 키: '/api/todos?status=completed' -> fetcher 함수에 url로 전달됨
const { data: swrData, error: swrError } = useSWR('/api/todos?status=completed', fetcher);
다음 시간에는 React Router를 사용한라우팅 시스템에 대해 알아보겠습니다. 사용자 요청에 따라 다른 페이지(컴포넌트)를 보여주는 방법과 중첩 라우트, 코드 스플리팅 등 관련 개념들을 함께 살펴보겠습니다.