1. Context란?
- 전역 데이터 (Global Data) : 프로젝트 전체적으로 사용하는 데이터
- 전역 데이터를 다룰 때 Props와 State만 사용하면 Props로 여러 번, 여러 곳에 내려줘야 한다는 문제점 존재
- Prop Drilling
: 드릴로 땅을 파듯이 상위 컴포넌트에서 하위 컴포넌트로 반복해서 Prop을 내려주는 상황
- Context를 사용하면 Props를 거치지 않고 여러 컴포넌트에 데이터를 넘겨줄 수 있음
- 리액트 Context를 사용하려면 우선 데이터를 공유할 범위를 정해야 함
-> Context.Provider라는 컴포넌트로 범위를 정해 줄 수 있음
-> Provider의 자손 컴포넌트에서는 Props를 거치지 않고 데이터를 자유롭게 쓸 수 있음
Context
- 많은 컴포넌트에서 사용하는 데이터를 반복적인 Prop 전달(Prop Drilling)없이 공유
2. Context로 데이터 내려주기
- Context를 만들려면 createContext라는 함수로 만들 수 있음
- 이 함수는 인수로 Context가 제공할 기본 값을 받음
- Context 객체에서 Provider라는 컴포넌트로 Context의 범위를 지정해 줄 수 있음
- 컴포넌트를 감싼 다음에 공유할 데이터를 value prop으로 내려 줄 수 있음
LocaleContext.js
import { createContext } from 'react';
const LocaleContext = createContext();
export default LocaleContext;
App.js
import { useCallback, useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import ReviewForm from './ReviewForm';
import { createReview, deleteReview, getReviews, updateReview } from '../api';
import useAsync from '../hooks/useAsync';
import LocaleContext from '../contexts/LocaleContext';
const LIMIT = 6;
function App() {
const [order, setOrder] = useState('createdAt');
const [offset, setOffset] = useState(0);
const [hasNext, setHasNext] = useState(false);
const [isLoading, loadingError, getReviewsAsync] = useAsync(getReviews);
const [items, setItems] = useState([]);
const sortedItems = items.sort((a, b) => b[order] - a[order]);
const handleNewestClick = () => setOrder('createdAt');
const handleBestClick = () => setOrder('rating');
const handleDelete = async (id) => {
const result = await deleteReview(id);
if (!result) return;
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
};
const handleLoad = useCallback(
async (options) => {
const result = await getReviewsAsync(options);
if (!result) return;
const { paging, reviews } = result;
if (options.offset === 0) {
setItems(reviews);
} else {
setItems((prevItems) => [...prevItems, ...reviews]);
}
setOffset(options.offset + options.limit);
setHasNext(paging.hasNext);
},
[getReviewsAsync]
);
const handleLoadMore = async () => {
await handleLoad({ order, offset, limit: LIMIT });
};
const handleCreateSuccess = (review) => {
setItems((prevItems) => [review, ...prevItems]);
};
const handleUpdateSuccess = (review) => {
setItems((prevItems) => {
const splitIdx = prevItems.findIndex((item) => item.id === review.id);
return [
...prevItems.slice(0, splitIdx),
review,
...prevItems.slice(splitIdx + 1),
];
});
};
useEffect(() => {
handleLoad({ order, offset: 0, limit: LIMIT });
}, [order, handleLoad]);
return (
<LocaleContext.Provider value="ko">
<div>
<div>
<button onClick={handleNewestClick}>최신순</button>
<button onClick={handleBestClick}>베스트순</button>
</div>
<ReviewForm
onSubmit={createReview}
onSubmitSuccess={handleCreateSuccess}
/>
<ReviewList
items={sortedItems}
onDelete={handleDelete}
onUpdate={updateReview}
onUpdateSuccess={handleUpdateSuccess}
/>
{hasNext && (
<button disabled={isLoading} onClick={handleLoadMore}>
더 보기
</button>
)}
{loadingError?.message && <span>{loadingError.message}</span>}
</div>
</LocaleContext.Provider>
);
}
export default App;
2. Context 값에 State 사용하기
- 다국어 기능 만들기
- App 컴포넌트에서 locale이라는 State를 만들고, Context에 value prop으로 내려 줌
- LocaleSelect라는 컴포넌트를 만듬 (옵션을 선택하면 locale값이 바뀜)
LocaleSelect.js
function LocaleSelect({ value, onChange}) {
const handleChange = (e) => onChange(e.target.value);
return (
<select value={value} onChange={handleChange}>
<option value="ko">한국어</option>
<option value="en">English</option>
</select>
);
}
export default LocaleSelect;
App.js
import { useCallback, useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import ReviewForm from './ReviewForm';
import { createReview, deleteReview, getReviews, updateReview } from '../api';
import useAsync from '../hooks/useAsync';
import LocaleContext from '../contexts/LocaleContext';
import LocaleSelect from './LocaleSelect';
const LIMIT = 6;
function App() {
const [order, setOrder] = useState('createdAt');
const [offset, setOffset] = useState(0);
const [hasNext, setHasNext] = useState(false);
const [isLoading, loadingError, getReviewsAsync] = useAsync(getReviews);
const [items, setItems] = useState([]);
const [locale, setLocale] = useState('ko');
const sortedItems = items.sort((a, b) => b[order] - a[order]);
const handleNewestClick = () => setOrder('createdAt');
const handleBestClick = () => setOrder('rating');
const handleDelete = async (id) => {
const result = await deleteReview(id);
if (!result) return;
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
};
const handleLoad = useCallback(
async (options) => {
const result = await getReviewsAsync(options);
if (!result) return;
const { paging, reviews } = result;
if (options.offset === 0) {
setItems(reviews);
} else {
setItems((prevItems) => [...prevItems, ...reviews]);
}
setOffset(options.offset + options.limit);
setHasNext(paging.hasNext);
},
[getReviewsAsync]
);
const handleLoadMore = async () => {
await handleLoad({ order, offset, limit: LIMIT });
};
const handleCreateSuccess = (review) => {
setItems((prevItems) => [review, ...prevItems]);
};
const handleUpdateSuccess = (review) => {
setItems((prevItems) => {
const splitIdx = prevItems.findIndex((item) => item.id === review.id);
return [
...prevItems.slice(0, splitIdx),
review,
...prevItems.slice(splitIdx + 1),
];
});
};
useEffect(() => {
handleLoad({ order, offset: 0, limit: LIMIT });
}, [order, handleLoad]);
return (
<LocaleContext.Provider value={locale}>
<div>
<LocaleSelect value={locale} onChange={setLocale} />
<div>
<button onClick={handleNewestClick}>최신순</button>
<button onClick={handleBestClick}>베스트순</button>
</div>
<ReviewForm
onSubmit={createReview}
onSubmitSuccess={handleCreateSuccess}
/>
<ReviewList
items={sortedItems}
onDelete={handleDelete}
onUpdate={updateReview}
onUpdateSuccess={handleUpdateSuccess}
/>
{hasNext && (
<button disabled={isLoading} onClick={handleLoadMore}>
더 보기
</button>
)}
{loadingError?.message && <span>{loadingError.message}</span>}
</div>
</LocaleContext.Provider>
);
}
export default App;
3. Context 코드 분리하기
- locale State랑 Context를 한곳에 모아서 관리
- 컴포넌트랑 커스텀 Hook을 사용해서 Context 코드를 정리
-> 다른 곳에서는 State값을 직접 참조하지 못하고 반드시 Context를 통해서만 쓸 수 있음
LocaleContext.js
import { createContext, useContext, useState } from 'react';
const LocaleContext = createContext();
export function LocaleProvider({ defaultValue = 'ko', children }) {
const [locale, setLocale] = useState(defaultValue);
return (
<LocaleContext.Provider value={{ locale, setLocale }}>
{children}
</LocaleContext.Provider>
);
}
export function useLocale() {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
}
const { locale } = context;
return locale;
}
export function useSetLocale() {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
}
const { setLocale } = context;
return setLocale;
}
4. 다국어 기능 완성하기
- 언어를 변경할 때마다 텍스트가 바뀌는 기능
- 사전, Dictionary 같은 자료구조 사용, 언어가 바뀔때마다 사전만 바꿔주면 됨
- 사전 형태의 자료구조랑 리액트 커스텀 Hook, Context를 활용해서 번역기능 만들기
useTranslate.js
import { useLocale } from '../contexts/LocaleContext';
const dict = {
ko: {
'confirm button': '확인',
'cancel button': '취소',
'edit button': '수정',
'delete button': '삭제',
},
en: {
'confirm button': 'OK',
'cancel button': 'Cancel',
'edit button': 'Edit',
'delete button': 'Delete',
},
};
function useTranslate() {
const locale = useLocale();
const translate = (key) => dict[locale][key] || '';
return translate;
}
export default useTranslate;
ReviewList.js
import { useState } from 'react';
import useTranslate from '../hooks/useTranslate';
import Rating from './Rating';
import ReviewForm from './ReviewForm';
import './ReviewList.css';
function formatDate(value) {
const date = new Date(value);
return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`;
}
function ReviewListItem({ item, onDelete, onEdit }) {
const t = useTranslate();
const handleDeleteClick = () => {
onDelete(item.id);
};
const handleEditClick = () => {
onEdit(item.id);
};
return (
<div className="ReviewListItem">
<img className="ReviewListItem-img" src={item.imgUrl} alt={item.title} />
<div>
<h1>{item.title}</h1>
<Rating value={item.rating} />
<p>{formatDate(item.createdAt)}</p>
<p>{item.content}</p>
<button onClick={handleEditClick}>{t('edit button')}</button>
<button onClick={handleDeleteClick}>{t('delete button')}</button>
</div>
</div>
);
}
function ReviewList({ items, onUpdate, onUpdateSuccess, onDelete }) {
const [editingId, setEditingId] = useState(null);
const handleCancel = () => setEditingId(null);
return (
<ul>
{items.map((item) => {
if (item.id === editingId) {
const { id, imgUrl, title, rating, content } = item;
const initialValues = { title, rating, content, imgFile: null };
const handleSubmit = (formData) => onUpdate(id, formData);
const handleSubmitSuccess = (review) => {
onUpdateSuccess(review);
setEditingId(null);
};
return (
<li key={item.id}>
<ReviewForm
initialValues={initialValues}
initialPreview={imgUrl}
onSubmit={handleSubmit}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancel}
/>
</li>
);
}
return (
<li key={item.id}>
<ReviewListItem
item={item}
onDelete={onDelete}
onEdit={setEditingId}
/>
</li>
);
})}
</ul>
);
}
export default ReviewList;
ReviewForm.js
import { useState } from 'react';
import useAsync from '../hooks/useAsync';
import useTranslate from '../hooks/useTranslate';
import FileInput from './FileInput';
import RatingInput from './RatingInput';
import './ReviewForm.css';
const INITIAL_VALUES = {
title: '',
rating: 0,
content: '',
imgFile: null,
};
function ReviewForm({
initialValues = INITIAL_VALUES,
initialPreview,
onCancel,
onSubmit,
onSubmitSuccess,
}) {
const t = useTranslate();
const [values, setValues] = useState(initialValues);
const [isSubmitting, submittingError, onSubmitAsync] = useAsync(onSubmit);
const handleChange = (name, value) => {
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
const handleInputChange = (e) => {
const { name, value } = e.target;
handleChange(name, value);
};
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('title', values.title);
formData.append('rating', values.rating);
formData.append('content', values.content);
formData.append('imgFile', values.imgFile);
const result = await onSubmitAsync(formData);
if (!result) return;
const { review } = result;
setValues(INITIAL_VALUES);
onSubmitSuccess(review);
};
return (
<form className="ReviewForm" onSubmit={handleSubmit}>
<FileInput
name="imgFile"
value={values.imgFile}
initialPreview={initialPreview}
onChange={handleChange}
/>
<input name="title" value={values.title} onChange={handleInputChange} />
<RatingInput
name="rating"
value={values.rating}
onChange={handleChange}
/>
<textarea
name="content"
value={values.content}
onChange={handleInputChange}
/>
{onCancel && <button onClick={onCancel}>{t('cancel button')}</button>}
<button disabled={isSubmitting} type="submit">
{t('confirm button')}
</button>
{submittingError && <div>{submittingError.message}</div>}
</form>
);
}
export default ReviewForm;
5. Context 정리
1) Context 만들기
- Context는 createContext라는 함수를 통해 만들 수 있음
import { createContext } from 'react';
const LocaleContext = createContext();
- 기본값 설정 가능
import { createContext } from 'react';
const LocaleContext = createContext('ko');
2) Context 적용하기
- Context를 쓸 때는 반드시 값을 공유할 범위를 정하고 써야 함
- 범위는 Context 객체에 있는 Provider 라는 컴포넌트로 정해줄 수 있음
- Provider의 value prop으로 공유할 값을 내려주면 됨
import { createContext } from 'react';
const LocaleContext = createContext('ko');
function App() {
return (
<div>
... 바깥의 컴포넌트에서는 LocaleContext 사용불가
<LocaleContext.Provider value="en">
... Provider 안의 컴포넌트에서는 LocaleContext 사용가능
</LocaleContext.Provider>
</div>
);
}
3) Context 값 사용하기
- useContext라는 Hook을 사용하면 값을 가져와 사용할 수 있음
- 인수로는 사용할 Context를 넘겨주면 됨
import { createContext, useContext } from 'react';
const LocaleContext = createContext('ko');
function Board() {
const locale = useContext(LocaleContext);
return <div>언어: {locale}</div>;
}
function App() {
return (
<div>
<LocaleContext.Provider value="en">
<Board />
</LocaleContext.Provider>
</div>
);
}
4) State, Hook와 함께 사용하기
- Procider역할을 하는 컴포넌트를 하나 만들고, 여기서 State를 만들어서 value로 넘겨줄 수 있음
- useContext를 사용해서 값을 가져오는 커스텀 Hook을 만들 수도 있음
=> Context에서 사용하는 State 값은 반드시 우리가 만든 함수를 통해서만 쓸 수 있기 떄문에 안전한 코드를 작성하는데 도움이 됨
import { createContext, useContext, useState } from 'react';
const LocaleContext = createContext({});
export function LocaleProvider({ children }) {
const [locale, setLocale] = useState();
return (
<LocaleContext.Provider value={{ locale, setLocale }}>
{children}
</LocaleContext.Provider>
);
}
export function useLocale() {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
}
const { locale } = context;
return locale;
}
export function useSetLocale() {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
}
const { setLocale } = context;
return setLocale;
}
6. 상태 관리
상태 관리 (state Management)
- 화면에서 사용하는 데이터를 관리하는 것
Flux, Redux
- 데이터의 변경을 한 곳에서 하면서 흐름을 정리
React Query, SWR
- 서버 상태 관리를 알아서 해 주는 라이브러리
- 클라이언트 상태와 서버 상태를 구분 (기준 : 데이터의 출처)
- 프론트 개발자들은 클라이언트 상태 관리에만 집중하고 서버 상태는 서버랑 동기화
Recoil
- 전역적으로 쓸 수 있는 useState
- 공유할 데이터를 컴포넌트 구조와 별개로 분리
- Atom : 어디서나 갖다 쓸 수 있는 State
'react' 카테고리의 다른 글
[소플] ch4. 엘리먼트 렌더링 (1) | 2023.10.27 |
---|---|
[코드잇] 리액트로 웹 사이트 만들기 (0) | 2023.09.28 |
[코드잇] 리액트로 데이터 다루기 (4) (0) | 2023.09.24 |
[코드잇] 리액트로 데이터 다루기(3) (0) | 2023.09.04 |
[코드잇] 리액트로 데이터 다루기(2) (0) | 2023.09.04 |