React

Context API와 상태관리 역사

leeeyez 2024. 7. 28. 17:04

 

다국어 기능 (한국어,영어 선택 시 화면 언어 바뀌게) 개발로 알아보는 react context api

공통 조상인 app 컴포넌트에서 state를 내려주면 됨, 하지만 button 컴포넌트를 생성하고 여기에 props를 내려주려면 너무 많은 컴포넌트들을 거쳐야한다는 단점이 있음

보통 최상위 부모 컴포넌트에서 관리하는 상태 데이터들을 전역 데이터라고 정의할 수 있는데, 이런 전역 데이터를 props로 하위 컴포넌트로 내려주는게 드릴로 땅을 파는 것 같다고 해서 이러한 문제점을 Prop Drilling이라고 한다

Prop Drilling : 드릴로 땅을 파듯이 상위 컴포넌트에서 하위 컴포넌트로 반복해서 prop을 내려주는 상황

해결하기 위해 react context 사용

= 맥락(context)

= 상황에 대한 정보 (사용자가 영어를 선택한 상황, 한국어를 선택한 상황)

 

context를 적용할 범위를 지정해줘야함 -> <Context.Provider />로 적용 범위 지정 

Provider의 자손 컴포넌트에서는 prop을 거치지 않고도 데이터를 자유롭게 쓸 수 있다

 

Context : 많은 컴포넌트에서 사용하는 데이터를 반복적인 Prop 전달(Prop Drilling) 없이 공유

 

contexts 라는 파일 만들기

LocaleContext.js 라는 파일 만들기

createContext() : context가 제공할 기본값을 받는다

import { createContext } from 'react';

const LocaleContext = createContext();

export default LocaleContext;

 

App.js의 최상위 컴포넌트로 LocalContext.Provider를 추가 (감싸주기)

value prop으로 공유할 데이터를 지정해주면 된다 value={'ko'}

// ...
import LocaleContext from '../contexts/LocaleContext';

// ...

function App() {
  // ...

  return (
    <LocaleContext.Provider value="ko">
      ...
    </LocaleContext.Provider>
  );
}

export default App;

 

원하는 컴포넌트에서 useContext 훅을 사용하면 전역 상태(공유한 데이터)를 불러올 수 있다

const locale = useContext(LocaleContext); -> 받아온 언어 값 알 수 있음

import { useContext, useState } from 'react';
import LocaleContext from '../contexts/LocaleContext';
// ...

function FoodListItem({ item, onEdit, onDelete }) {
  const locale = useContext(LocaleContext);
  // ...

  return (
    ...
    <p>언어: {locale}</p>
    ...
  );
}

// ...

 

 

LocaleContext 파일 수정 : LocaleProvider 컴포넌트와 useLocale, useSetLocale이라는 커스텀 Hook 만들기

import { createContext, useContext, useState } from 'react';

const LocaleContext = createContext();

export function LocaleProvider({ defaultValue, children }) {
  const [locale, setLocale] = useState(defaultValue);

  return (
    <LocaleContext.Provider value={{ locale, setLocale }}>
      {children}
    </LocaleContext.Provider>
  )
}

export function useLocale() {
  const { locale } = useContext(LocaleContext);
  return locale;
}

export function useSetLocale() {
  const { setLocale } = useContext(LocaleContext);
  return setLocale;
}

LocaleProvider라는 컴포넌트에서 locale 상태를 만들고, 이걸 LocaleContext.Provider에 value prop으로 setLocale() 함수와 함께 객체로 내려준다

기본값은 defaultValue라는 prop으로 받아와서 useState에 넘겨준다.

useLocale이라는 Hook에서는 LocaleContext의 loacale이라는 값만 받아서 리턴하고,

useSetLocale이라는 Hook에서는 LocaleContext에서 setLocale이라는 값을 받아 리턴한다

그리고 Context를 직접 사용하는 걸 막기 위해서 export default는 제거

 

src/hooks/useTranslate.js

import { useLocale } from '../contexts/LocaleContext';

const dict = {
  ko: {
    'edit button': '수정',
    'delete button': '삭제',
  },
  en: {
    'edit button': 'Edit',
    'delete button': 'Delete',
  },
};

function useTranslate() {
  const locale = useLocale();
  const translate = (key) => dict[locale][key] || '';
  return translate;
}

export default useTranslate;

useTranslate이라는 hook을 만들어 '번역 키를 받아서 현재 언어에 해당하는 문자열 리턴하는 함수'를 리턴

 

src/components/LocaleSelect.js

import { useLocale, useSetLocale } from '../contexts/LocaleContext';

function LocaleSelect() {
  const locale = useLocale();
  const setLocale = useSetLocale();

  const handleChange = (e) => setLocale(e.target.value);

  return (
    <select value={locale} onChange={handleChange}>
      <option value="ko">한국어</option>
      <option value="en">English</option>
    </select>
  );
}

export default LocaleSelect;

LocaleSelect라는 컴포넌트를 만들어서 언어 선택

useLocale()과 useSetLocale()을 사용해서 locale, setLocale 가져오고, 이걸로 <select> 태그 안에서 언어값 선택

 

src/components/App.js

// ...
import { LocaleProvider } from '../contexts/LocaleContext';
import LocaleSelect from './LocaleSelect';
// ...

function App() {
  // ...
  return (
    <LocaleProvider defaultValue="ko">
       ...
       <LocaleSelect />
       ...
    </LocaleProvider>
  );
}

export default App;

기존에 Context를 직접 사용하던 것을 LocaleProvider로 바꿔준다

LocaleSelect 컴포넌트도 추가

 

src/components/FoodList.js

// ...
import useTranslate from '../hooks/useTranslate';
// ...

function FoodListItem({ item, onEdit, onDelete }) {
  // ...
  const t = useTranslate();
  // ...
  return (
    <div className="FoodListItem">
      <img src={imgUrl} alt={title} />
      <div>{title}</div>
      <div>{calorie}</div>
      <div>{content}</div>
      <div>{formatDate(createdAt)}</div>
      <button onClick={handleEditClick}>{t('edit button')}</button>
      <button onClick={handleDeleteClick}>{t('delete button')}</button>
    </div>
  );
}

useTranslate라는 hook으로 번역 함수를 가져오고, 이를 각 버튼에 적용

 

요약

Context 만들기

Context는 createContext라는 함수를 통해 만들 수 있다

import { createContext } from 'react';

const LocaleContext = createContext();

이때 createContext 파라미터로 기본값을 넣어줄 수도 있다(createContext('ko'))

 

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>
  );
}

 

Context 값 사용하기

useContext라는 Hook을 사용하면 값을 가져와 사용할 수 있다

이때 argument로는 사용할 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>
  );
}

 

State, Hook과 함께 활용하기

Provider 역할을 하는 컴포넌트를 만들고 여기서 state를 만들어 value로 넘겨줄 수 있다

useLocale과 같이 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;
}

 

 

 

상태관리 역사

화면에서 사용하는 데이터들을 관리하는 것을 상태관리(State Management)라고 한다.

Prop Drilling이라는 문제 : 최상위 데이터를 멀리 떨어진 컴포넌트에 전달하려면 prop을 반복적으로 내려줘야한다는 문제

초창기 react 개발팀은 이런 문제를 해결하기 위해 context를 제공했다.(2013 - 이건 현재 useContext와는 다른 구버전) 하지만 데이터를 여러 컴포넌트가 불러 쓰면서, 어떤 컴포넌트가 언제, 어떻게 데이터를 변경했는지 알기 어려워지는 문제가 발생했다.

 

페이스북의 Flux 라이브러리 개발 : 데이터 변경을 한 곳에서 하면서 흐름을 정리

카운터 직원이 주문서를 순서대로 정리해서 주방에 전달하는 것처럼 (한 방향, 한 곳에서 정리)

데이터가 어떻게 변화하는지 알기 위해서 Dispatcher에서 Store로 전달되는 액션을 살펴보기만 하면 된다

 

2015년, Dan Abramov가 Flux의 영향을 받아 좀 더 단순한 라이브러리인 Redux를 발표 -> 인기를 얻어 5개월만에 react 코어팀에 합류 (이후 redux의 영향을 받은 react 기술이 개발되기도함)

 

2018년 React Context(현재의 Context) 개발, 하지만 Redux의 인기가 계속되어 많은 기업에서 Redux를 활용 -> Redux에 너무 많은 데이터가 저장되게 됨 ->  Redux를 통해 비동기 처리를 하려면 알아보기 힘듦 (로딩처리, 에러처리)

 

Client-state와 Server-state를 구분하기 시작 (두가지를 구분하는 기준은 데이터의 출처)

 

2019년 서버 상태 관리를 알아서 해주는 라이브러리들이 등장 - React QuerySWR

(request를 정해놓으면 서버랑 알아서 동기화를 하기 때문에 편하게 서버 상태 관리 가능)

 

2020년 페이스북에서 recoil 라이브러리 발표 - 쉽게 말하면 전역적으로 쓸 수 있는 useState

 

Context는 편리하지만, 데이터가 늘어날 때마다 Context도 하나씩 늘어난다는 문제점이 있다.

그래서 하나의 Context에 모든 데이터를 모아 관리하려고 했더니, 하나의 데이터가 바뀔떄마다 상관 없는 컴포넌트들도 함께 재랜더링된다는 문제점이 발생

그리고 데이터의 구조가 컴포넌트 구조와 섞임

데이터들이 자연스럽게 컴포넌트 트리 구조에 따라 배치되는데, 가끔 데이터들끼리 조합해서 새로운 값을 만들어내면 이 구조와 맞지 않는 경우가 있다. 이런 경우에는 모든 데이터가 결국 최상위로 모이게 된다. 그래서 recoil 개발자들은 이러한 데이터들을 그냥 아예 따로 뺴면 어떨까? 하고 생각하게 된다. 공유할 데이터를 컴포넌트 구조와 따로 분리하는 것. 이렇게 분리한 데이터를 atom이라고 한다. 쉽게 말해 atom은 어디서나 갖다 쓸 수 있는 데이터라고 생각하면 된다.

react context를 사용하다가 어려운 점이 생긴다면, 다른 라이브러리를 사용하는 것 추천!

'React' 카테고리의 다른 글

사용자 정보 관리/소셜로그인  (0) 2025.03.26
zustand, recoil, jotai  (0) 2024.07.30
S3 배포를 통해 알아보는 React 배포 과정  (0) 2024.07.29
React Query (1)  (0) 2024.07.29