Context란?
Context는
여러 컴포넌트가 공통으로 사용하는 값을
컴포넌트 트리 어디서든 쉽게 접근할 수 있도록 만들어주는 전역 데이터 저장소
왜 Context를 써야 할까?
리액트 애플리케이션에서는 데이터 전달이 기본적으로 부모 → 자식 props 방식
여러 컴포넌트에서 공통으로 필요한 데이터를 매번 props로 전달하면 코드가 지저분해지는 props drilling 발생!!
Context 패턴 정석 구성
- store에서 context 선언
- Provider로 컴포넌트 트리를 감싸서 전역 값 전달
- 커스텀 훅으로 context 값 가져오기
Next.js + TypeScript 이용한 Context 정리
타입(interface) 선언하기
=> 컨텍스트로 관리할 상태와 그 상태를 변경할 함수의 타입을 정의
interface ColorContextType {
color: string;
subcolor: string;
changeColor: (color: string) => void;
changeSubcolor: (subcolor: string) => void;
}
store /
- context 선언
import { createContext } from "react";
const ColorContext = createContext<ColorContextType | undefined>(undefined);
- createContext()를 통해 Context 생성.
- 초기값을 undefined로 두는 이유: Provider 없이 context를 사용했을 때 예외처리를 위함
- Provider 컴포넌트 만들기
import { useState, ReactNode } from "react";
export const ColorProvider = ({ children }: { children: ReactNode }) => {
const [color, setColor] = useState<string>("black");
const [subcolor, setSubcolor] = useState<string>("red");
const changeColor = (c: string) => setColor(c);
const changeSubcolor = (c: string) => setSubcolor(c);
return (
<ColorContext.Provider value={{ color, subcolor, changeColor, changeSubcolor }}>
{children}
</ColorContext.Provider>
);
};
- 상태(state)와 상태를 변경하는 함수 선언
- Context의 value로 넘겨주기
- children은 ReactNode 타입으로 받기 (타입스크립트 환경에서 필수)
- 감싸진 컴포넌트들에 context 값을 공급하기 위해 children 사용
- 커스텀 훅으로 Context 값 가져오기
import { useContext } from "react";
export const useColorContext = () => {
const context = useContext(ColorContext);
if (!context) {
throw new Error("useColorContext must be used within a ColorProvider");
}
return context;
};
- useContext()로 context 값을 가져옴.
- 값이 없으면 (즉, Provider로 감싸지 않았으면) 에러 발생.
- 가져올때마다 에러처리 하지 않아도 바로 이용 가능!!!
실제 사용 예시
<ColorProvider>
<App />
</ColorProvider>
const { color, changeColor } = useColorContext();
=> App 내부 어디서든 사용 가능
1 | 타입 선언 | interface |
2 | Context 생성 | createContext() |
3 | Provider 생성 | ColorProvider |
4 | 커스텀 훅 | useColorContext() |
실전_북마크 예제 작성
: 도서 목록에서 북마크 클릭 시 도서 저장 및 / 삭제 / 완전 삭제 기능까지 구현
북마크 구성요소 정리
store/book-mark.ts | 북마크 전역 상태 관리 (context + provider + hook) |
components/book_mark/BookMarkItem.tsx | 북마크 목록 테이블의 단일 행 컴포넌트 / 재사용 가능한 UI 컴포넌트 모음 |
app/book_mark/page.tsx | 북마크 목록 페이지 (목록 + 전체 삭제 버튼) |
app/book_mark/layout.tsx | 북마크 페이지 전용 레이아웃 |
utils/http-commons.ts | axios 기본 인스턴스 설정 |
types/book.ts
export interface Book {
isbn: string;
title: string;
author: string;
price: number;
img: string;
}
=> 북마크에 사용할 도서 데이터 타입 정의
store/book-mark.ts
"use client";
import { createContext, useContext, useState, useMemo, ReactNode, useCallback } from "react";
import { Book } from "@/types/book";
// context value 타입
interface BookMarkContextType {
bookMark: Book[];
registBookMark: (book: Book) => void;
removeBookMark: (isbn: string) => void;
clearBookMark: () => void;
}
// context 생성
const BookMarkContext = createContext<BookMarkContextType | undefined>(undefined);
// Provider 컴포넌트
export const BookMarkProvider = ({ children }: { children: ReactNode }) => {
const [bookMark, setBookMark] = useState<Book[]>([]);
const registBookMark = useCallback((book: Book) => {
setBookMark((prev) => (prev.find((item) => item.isbn === book.isbn) ? prev : [...prev, book]));
}, []);
const removeBookMark = useCallback((isbn: string) => {
setBookMark((prev) => prev.filter((item) => item.isbn !== isbn));
}, []);
const clearBookMark = useCallback(() => {
setBookMark([]);
}, []);
// value를 useMemo로 캐싱해 리렌더 최적화
const value = useMemo(
() => ({ bookMark, registBookMark, removeBookMark, clearBookMark }),
[bookMark]
);
return <BookMarkContext.Provider value={value}>{children}</BookMarkContext.Provider>;
};
//커스텀 훅
export const useBookMarkContext = () => {
const context = useContext(BookMarkContext);
if (!context) {
throw new Error("useBookMarkContext must be used within a BookMarkProvider");
}
return context;
};
- 전역 상태를 context로 관리
- useMemo로 value 캐싱해 리렌더 최적화
- useCallback으로 함수 재생성 방지
- 커스텀 훅으로 에러 메시지와 context 가져오기 통일
커스텀 훅의 장점
- 매번 useContext + 에러처리 중복 안 해도 됨
- context 구조 변경되면 이 훅만 수정하면 됨
- 에러 메시지 일관성 유지
layout.tsx에서 Provider로 감싸기
전역 상태를 사용하려면 context provider로 컴포넌트 트리를 감싸야 함
import { BookMarkProvider } from "@/store/book-mark";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<BookMarkProvider>
{children}
</BookMarkProvider>
</body>
</html>
);
}
=> 이렇게 감싸주면 하위 페이지에서 useBookMarkContext()로 북마크 목록을 사용
북마크 목록 페이지
"use client";
import { useBookMarkContext } from "@/store/book-mark";
import BookMarkItem from "@/components/book_mark/BookMarkItem";
import styles from "./book_mark.module.scss";
// 북마크 목록 페이지 컴포넌트
export default function BookMarkPage() {
// context에서 북마크 목록과 전체 삭제 함수 가져오기
const { bookMark, clearBookMark } = useBookMarkContext();
return (
<div className={styles.bookList}>
{/* 전체 삭제 버튼 */}
<button onClick={clearBookMark}>모두 삭제</button>
{/* 도서 목록 테이블 */}
<table>
<thead>
<tr>
<th>이미지</th>
<th>번호</th>
<th>제목</th>
<th>저자</th>
<th>가격</th>
<th>비고</th>
</tr>
</thead>
<tbody>
{/* 북마크 목록 있을 때 */}
{bookMark.length > 0 ? (
bookMark.map((book) => (
<BookMarkItem key={book.isbn} book={book} />
))
) : (
// 북마크 없을 때 안내 메시지
<tr>
<td colSpan={6}>북마크 없음</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
useBookMarkContext() | 북마크 목록과 삭제 함수 가져옴 |
clearBookMark() | 전체 삭제 버튼 클릭 시 호출 |
bookMark.map() | 북마크 목록을 map 을 통하여 테이블로 출력 |
북마크 없을 때 | 안내 메시지 출력 |
스타일 | SCSS 모듈로 적용 |
'React' 카테고리의 다른 글
타입스크립트 사용법과 기본 타입 정리 (0) | 2025.05.26 |
---|---|
React Context API와 LocalStorage로 상태 관리 (0) | 2025.04.23 |
TanStack Query (React Query) 알아보기 (0) | 2025.04.21 |
Axios와 async/await (2) | 2025.04.16 |
React & Next.js 활용해보기 / CSR vs SSR (1) | 2025.04.14 |