컴포넌트 심화 · useEffect · useContext · useReducer
컴포넌트 설계의 핵심 원칙(단일 책임), children/render prop 패턴, 조건부 렌더링 3가지 방법, 그리고 React가 권장하는 합성 vs 상속을 다룹니다. Side Effect의 개념, useEffect의 3가지 의존성 배열 패턴, 클린업 함수, AbortController를 활용한 fetch 취소, 그리고 흔한 무한 루프 함정을 익힙니다. props drilling 문제, createContext/Provider/useContext 3단계, Context를 안전하게 감싸는 커스텀 훅, Context 적합·부적합 상황, 그리고 다크모드 패턴을 익힙니다. 복잡한 상태 로직을 useState 대신 useReducer로 분리하기 — action/dispatch/reducer 패턴, useState와의 선택 기준, 그리고 불변성을 유지한 상태 업데이트.
이 강의에서 배우는 것
- 1컴포넌트는 한 가지 역할만 한다는 단일 책임을 적용한다
- 2children과 render prop으로 유연한 컴포넌트를 만든다
- 3&&, 삼항, 컴포넌트 분리 — 조건부 렌더링 3가지를 구분한다
- 4상속 대신 합성으로 컴포넌트를 확장한다
- 5Side Effect 개념을 이해하고 useEffect 안에서 처리한다
- 6[] / [값] / 없음 — 3가지 의존성 패턴을 구분한다
- 7클린업 함수가 필요한 4가지 상황을 안다
- 8AbortController로 fetch를 안전하게 취소한다
- 9객체/배열 의존성으로 인한 무한 루프를 방지한다
- 10props drilling이 왜 문제인지 설명한다
- 11createContext → Provider → useContext 3단계를 작성한다
- 12useContext를 커스텀 훅으로 감싼다
- 13Context 적합·부적합 상황을 판단한다
- 14전역 테마 토글 패턴을 구현한다
- 15action/dispatch/reducer 3요소를 안다
- 16switch문으로 reducer를 작성한다
- 17useState와 useReducer의 선택 기준을 안다
- 18스프레드/map/filter로 불변성을 유지한다
컴포넌트 심화
컴포넌트 설계의 핵심 원칙(단일 책임), children/render prop 패턴, 조건부 렌더링 3가지 방법, 그리고 React가 권장하는 합성 vs 상속을 다룹니다.
1. 단일 책임 원칙
// ❌ 나쁜 예 — 하나의 컴포넌트가 너무 많은 일
function UserCard() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return (
<div>
<img src={user?.avatar} />
<h2>{user?.name}</h2>
<button onClick={() => alert('팔로우!')}>팔로우</button>
</div>
);
}
// ✅ 좋은 예 — 역할 분리
function UserAvatar({ src, alt }) { return <img src={src} alt={alt} />; }
function FollowButton({ onFollow }) { return <button onClick={onFollow}>팔로우</button>; }
function UserCard({ user, onFollow }) {
return (
<div>
<UserAvatar src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<FollowButton onFollow={onFollow} />
</div>
);
}2. children props
function Card({ children }) {
return (
<div style={{ border: '1px solid #ddd', padding: 16, borderRadius: 8 }}>
{children}
</div>
);
}
<Card>
<h2>제목</h2>
<p>내용이 여기에 들어갑니다.</p>
</Card>레이아웃·래퍼 컴포넌트에 자주 사용.
3. render prop 패턴
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData);
}, [url]);
return render(data);
}
<DataFetcher url="/api/users" render={(users) => (
<ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)} />4. 조건부 렌더링
&& 연산자 (단순)
{hasMessage && <p>{message}</p>}
// 주의: hasMessage가 0이면 "0"이 출력됨 → Boolean(hasMessage) 권장삼항 (둘 중 하나)
{isLoggedIn ? <button>로그아웃</button> : <button>로그인</button>}컴포넌트 분리 (복잡)
function UserProfile({ isLoading, error, user }) {
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return <div>{user.name}</div>;
}5. 합성 vs 상속
| 구분 | 합성 | 상속 |
|---|---|---|
| 방법 | children, props로 조합 | class extends |
| 유연성 | 높음 | 낮음 |
| React 권장 | ✅ | ❌ |
function Dialog({ title, message, children }) {
return (
<div className="dialog">
<h1>{title}</h1>
<p>{message}</p>
{children}
</div>
);
}
function WelcomeDialog() {
return (
<Dialog title="환영합니다!" message="React 심화 과정에 오신 것을 환영합니다.">
<button>시작하기</button>
</Dialog>
);
}useEffect
Side Effect의 개념, useEffect의 3가지 의존성 배열 패턴, 클린업 함수, AbortController를 활용한 fetch 취소, 그리고 흔한 무한 루프 함정을 익힙니다.
1. Side Effect란
렌더링 외에 컴포넌트가 외부와 상호작용하는 모든 것 — 데이터 fetch, DOM 직접 조작, 이벤트 리스너, 타이머, 구독.
React 렌더링은 순수해야 합니다(같은 props → 항상 같은 JSX). Side Effect는 useEffect 안에서.
2. 의존성 배열 — 3가지 패턴
패턴 1: 의존성 배열 없음 → 매 렌더링
useEffect(() => {
console.log('렌더링될 때마다 실행');
}); // ⚠️ 거의 사용 안 함패턴 2: 빈 배열 → 마운트 시 한 번
useEffect(() => {
fetchInitialData();
}, []);패턴 3: 값이 있는 배열 → 값이 바뀔 때마다
useEffect(() => {
fetchUser(userId);
}, [userId]);3. 클린업 함수
useEffect(() => {
const timerId = setInterval(() => console.log('1초마다'), 1000);
return () => {
clearInterval(timerId);
console.log('타이머 정리!');
};
}, []);| 상황 | 클린업 |
|---|---|
| setInterval/setTimeout | clearInterval/clearTimeout |
| addEventListener | removeEventListener |
| WebSocket | socket.close() |
| fetch | AbortController.abort() |
4. 데이터 fetch 패턴
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
try {
setIsLoading(true);
setError(null);
const r = await fetch(`/users/${userId}`, { signal: controller.signal });
if (!r.ok) throw new Error('실패');
setUser(await r.json());
} catch (err) {
if (err.name !== 'AbortError') setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchUser();
return () => controller.abort();
}, [userId]);
if (isLoading) return <p>로딩 중...</p>;
if (error) return <p>오류: {error}</p>;
return <p>{user?.name}</p>;
}5. 무한 루프 주의
// ❌ 무한 루프 — count 변경 → effect → count 변경 → ...
useEffect(() => {
setCount(count + 1);
}, [count]);
// ✅ 함수형 업데이트
useEffect(() => {
setCount(prev => prev + 1);
}, []);
// ❌ 객체/배열 의존성 — 매 렌더링마다 새 참조
useEffect(() => {
fetchData(options);
}, [options]);
// ✅ useMemo / useCallback (강의06)으로 참조 유지useContext
props drilling 문제, createContext/Provider/useContext 3단계, Context를 안전하게 감싸는 커스텀 훅, Context 적합·부적합 상황, 그리고 다크모드 패턴을 익힙니다.
1. props drilling 문제
App (user 데이터)
└─ Page
└─ Section
└─ Header
└─ UserAvatar ← 여기서 user 사용중간 컴포넌트들이 user를 사용하지 않지만 계속 받아 넘겨야 합니다.
2. createContext / useContext
import { createContext, useContext, useState } from 'react';
// 1. Context 생성
const UserContext = createContext(null);
// 2. Provider — value로 데이터 제공
function App() {
const [user, setUser] = useState({ name: '김철수' });
return (
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
);
}
// 3. Consumer — useContext로 어디서든 접근
function UserAvatar() {
const user = useContext(UserContext);
return <img alt={user.name} />;
}3. 커스텀 훅으로 감싸기
function useUser() {
const context = useContext(UserContext);
if (!context) throw new Error('useUser는 UserProvider 안에서');
return context;
}
function UserAvatar() {
const user = useUser();
return <img alt={user.name} />;
}더 명시적이고 Provider 누락을 즉시 감지.
4. Context 언제 쓸까?
| 적합 | 부적합 |
|---|---|
| 테마 (다크/라이트) | 자주 변경되는 데이터 |
| 로그인 사용자 정보 | 특정 컴포넌트만 쓰는 지역 상태 |
| 언어/지역 설정 | 성능이 중요한 데이터 |
| 전역 알림/토스트 |
Context 값이 바뀌면 구독하는 모든 컴포넌트가 리렌더링. 자주 바뀌는 데이터는 Zustand 같은 상태 관리 라이브러리.
5. 전역 테마 패턴
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
return useContext(ThemeContext);
}useReducer
복잡한 상태 로직을 useState 대신 useReducer로 분리하기 — action/dispatch/reducer 패턴, useState와의 선택 기준, 그리고 불변성을 유지한 상태 업데이트.
1. 문법
const [state, dispatch] = useReducer(reducer, initialState);| 요소 | 역할 |
|---|---|
| state | 현재 상태 |
| dispatch | action을 보내는 함수 |
| reducer | (state, action) => newState 순수 함수 |
| initialState | 초기 상태 |
2. action / dispatch / reducer
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const CLEAR_CART = 'CLEAR_CART';
function cartReducer(state, action) {
switch (action.type) {
case ADD_ITEM:
return { ...state, items: [...state.items, action.payload] };
case REMOVE_ITEM:
return { ...state, items: state.items.filter(i => i.id !== action.payload) };
case CLEAR_CART:
return { ...state, items: [] };
default:
return state;
}
}
function Cart() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (product) => dispatch({ type: ADD_ITEM, payload: product });
const removeItem = (id) => dispatch({ type: REMOVE_ITEM, payload: id });
}3. useState vs useReducer
| 상황 | 권장 |
|---|---|
| 단순한 값 하나 | useState |
| 여러 state가 서로 연관 | useReducer |
| 다음 상태가 이전 상태에 의존 | useReducer |
| 상태 업데이트 로직이 복잡 | useReducer |
| 테스트가 중요 (reducer는 순수) | useReducer |
// useState로 분산되면 복잡
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [discountRate, setDiscountRate] = useState(0);
const [appliedCoupon, setAppliedCoupon] = useState(null);
// useReducer로 묶음
const [cartState, dispatch] = useReducer(cartReducer, {
items: [], total: 0, discountRate: 0, appliedCoupon: null,
});4. 불변성 유지
// ❌ 직접 변경 — 리렌더링 안 됨!
case ADD_ITEM:
state.items.push(action.payload);
return state;
// ✅ 새 배열 생성
case ADD_ITEM:
return { ...state, items: [...state.items, action.payload] };
// ✅ 특정 항목 수정 — map으로 새 배열
case UPDATE_QUANTITY:
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
};
// ✅ 특정 항목 제거 — filter
case REMOVE_ITEM:
return { ...state, items: state.items.filter(i => i.id !== action.payload) };댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침