본문 바로가기

react

[코드잇] 리액트로 데이터 다루기(5)

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