React

[React] 리액트 context API로 상태 관리 하기

solfa 2024. 2. 17. 06:46

Context API란?

Context API는 React에서 전역적인 상태를 관리하고 컴포넌트 간에 데이터를 전달하는 데 사용되는 기능이다. 이를 통해 props 전달이 깊은 컴포넌트 트리를 통해 이루어지는 것을 피할 수 있다. 

 

Props Drilling Problem을 해결하기 위해 생김! 컴포넌트 트리에서 Context라는 거대한 공통 조상을 만들고 그 Context로 부터 데이터를 제공을 받는 방식이다. 별도의 Store을 가지고 있는 FLUX와 비슷한 느낌이 있어서 최근에는 복잡한 문법을 가지고 만들어야 하는 Redux보다는 React의 기본 기능인 Context API를 쓰겠다는 움직임이 생기고 있다고 한다.

 

 

Context API의 핵심 개념

  1. Context: Context는 React 컴포넌트 트리에서 전역적으로 사용할 데이터를 제공한다. 데이터가 담긴 Context 객체를 생성하고 해당 데이터를 제공하는 컴포넌트를 정의한다.
  2. Provider: Context를 사용하여 전역 데이터를 제공하는 컴포넌트를 Provider라고 하는데 Provider는 하위 컴포넌트에게 Context의 값을 전달한다.
  3. Consumer: Consumer는 Context의 값을 사용하는 컴포넌트를 말한다. Consumer는 Provider의 하위 컴포넌트로서 Context의 값을 읽거나 구독할 수 있다.
  4. useContext Hook: useContext Hook은 함수형 컴포넌트에서 Context를 사용하기 위한 편리한 방법을 제공한다. 이 Hook을 사용하면 Consumer를 사용하지 않고도 Context의 값을 읽을 수 있다.

 

Context API의 장점과 단점

장점:

  1. 전역 상태 관리: Context API를 사용하면 전역 상태를 쉽게 관리할 수 있다. 이는 여러 컴포넌트 간에 데이터를 전달하고 상태를 공유해야 할 때 특히 유용하다.
  2. 깊은 컴포넌트 트리를 거치지 않고 데이터 전달: Context API를 사용하면 중첩된 컴포넌트 구조에서 props를 계속해서 전달할 필요가 없어진다. -> 코드를 더 깨끗하게 유지하고 유지보수성을 향상시킬 수 있음.
  3. 컴포넌트의 재사용성 향상: Context API를 사용하여 상태를 관리하면 해당 상태를 공유하는 여러 컴포넌트 사이에서 재사용성이 높아아짐.

단점:

  1. 성능 문제: Context API를 남용하면 성능 문제가 발생할 수 있다. 모든 컴포넌트가 상태 변경을 감지하고 다시 렌더링될 수 있기 때문! 이를 최적화하기 위해선 적절한 상태 분할과 최적화 기법이 필요함.
  2. 디버깅이 어려울 수 있음: Context를 사용하면 상태가 전역적으로 관리되므로 디버깅이 어려울 수 있다. 특히 어떤 컴포넌트에서 상태가 변경되었는지 추적하기 어려울 수 있다. -> 이를 위해 나온 게 퍼널...
  3. 복잡성 증가 가능성: Context API를 사용하면 상태 및 데이터 흐름이 더 복잡해질 수 있다. 대규모 프로젝트에서 특히 문제가 됨

 

이 글을 작성하는데 도움을 준 글

https://deku.posstree.com/ko/react/context-api/

 

[React] Context API

React에서 데이터를 다루는 개념중 하나인 Context API를 사용하는 방법에 대해서 알아봅시다.

deku.posstree.com

 

 


 

난 유형 테스트를 만들 때 전역 상태를 관리하기 위해 context API를 사용했다.

 

항목을 골랐을 때 변수나 불리언을 사용해 체크를 한 후 결과페이지에서 조건문으로 검사 후 페이지 보여주기

앞 페이지에서 선택한 항목은 어떻게 저장할까? → Context API 이용!

 

Context API 사용 예시:

  1. createContext 함수를 사용하여 Context 객체를 생성한다.
  2. Context를 제공할 컴포넌트를 만들고, 해당 컴포넌트의 하위 컴포넌트들에게 데이터를 제공하기 위해 Provider를 사용한다.
  3. Consumer를 사용하여 전역 데이터를 사용할 컴포넌트를 작성한다. 이러한 컴포넌트는 Provider의 하위에 있어야 한다.
  4. 필요한 경우 useContext Hook을 사용하여 함수형 컴포넌트에서 간편하게 Context 값을 사용할 수 있다.

 

1. 선택한 항목을 저장할 Quiz 상태를 만들고 해당 상태를 전역적으로 사용할 수 있는 Context를 생성한다

// QuizContext.js
import React, { createContext, useContext, useState } from 'react';

const QuizContext = createContext();
// createContext() 함수를 사용하여 새로운 Context를 생성 -> 이 Context가 Quiz 상태를 관리

export const useQuizContext = () => useContext(QuizContext);
// useQuizContext 함수는 해당 Context의 값을 사용하기 위한 커스텀 훅임! 
// useContext를 사용하여 QuizContext의 값을 가져옴

export const QuizProvider = ({ children }) => {
// QuizProvider 컴포넌트 : Quiz 상태 관리 + 해당 상태를 전역적으로 사용할 수 있도록 Context를 제공
// 이 컴포넌트는 children prop을 받아 자식 컴포넌트를 감싸고 해당 컴포넌트들이 QuizContext의 값을 사용할 수 있도록 함

  const [selectedItem, setSelectedItem] = useState(null);
  // useState를 사용하여 selectedItem 상태 정의
  // 이 상태는 현재 선택된 항목을 나타냄 초기값은 null로 설정

  const setQuizItem = (item) => {
  // setQuizItem 함수는 선택된 항목을 변경하기 위한 함수, selectedItem 상태를 업데이트
    setSelectedItem(item);
  };

  return (
    <QuizContext.Provider value={{ selectedItem, setQuizItem }}>
    // QuizContext.Provider를 사용하여 QuizContext의 값을 제공함 
    // 이 컴포넌트의 value prop으로는 selectedItem 상태와 setQuizItem 함수를 전달
	// 이렇게 하면 해당 값을 사용하는 모든 하위 컴포넌트들이 이에 접근할 수 있게 됨
      {children}
    </QuizContext.Provider>
  );
};

 

- QuizProvider로 감싼 모든 하위 컴포넌트들은 useQuizContext 훅을 사용하여 QuizContext의 값을 읽거나 업데이트할 수 있다.

 

 

2. 앱의 최상위 컴포넌트인 App 컴포넌트를 QuizProvider로 감싸서 QuizContext를 앱 전체에서 사용할 수 있도록 한다

// App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { QuizProvider } from './QuizContext';
import Quiz1 from './Quiz1';
import Quiz2 from './Quiz2';
import Result from './Result';

const App = () => {
  return (
    <Router>
      <QuizProvider>
        <Routes>
          <Route path="/" element={<Quiz1 />} />
          <Route path="/quiz2" element={<Quiz2 />} />
          <Route path="/result" element={<Result />} />
        </Routes>
      </QuizProvider>
    </Router>
  );
};

export default App;

 

3. Quiz1 컴포넌트에서 선택한 항목을 QuizContext에 저장하고, Result 컴포넌트에서 해당 항목을 가져와서 결과를 보여준다

// Quiz1.js
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuizContext } from './QuizContext';
import { Wrapper, Square, Count, Progress, QText, SelectButton } from 'Styles/styles';
import square from 'Images/main/Rectangle 4.svg';

const Quiz1 = () => {
  const navigate = useNavigate();
  const { setQuizItem } = useQuizContext();

  const goQuiz = () => {
    setQuizItem('고양이'); // 사용자가 선택한 항목 설정
    navigate("/quiz2");
  };

  return (
    <Wrapper>
      <Square src={square} alt='' />
      <Count>1/10</Count>
      <Progress value="10" min="0" max="100" />
      <QText>Q.1<br/>당신의 솔미 모에화는</QText>
      <SelectButton onClick={goQuiz}>햄스터 🐹</SelectButton>
      <SelectButton onClick={goQuiz}>다람쥐 🐿️</SelectButton>
      <SelectButton onClick={goQuiz}>고양이 🐈</SelectButton>
      <SelectButton onClick={goQuiz}>강아지 🐶</SelectButton>
    </Wrapper>
  );
};

export default Quiz1;

 

+ + goQuiz 함수 내에서 사용자가 선택한 항목을 인자로 전달해야 함! 코드 수정

// Quiz1.js
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuizContext } from './QuizContext';
import { Wrapper, Square, Count, Progress, QText, SelectButton } from 'Styles/styles';
import square from 'Images/main/Rectangle 4.svg';

const Quiz1 = () => {
  const navigate = useNavigate();
  const { setQuizItem } = useQuizContext();

  const goQuiz = (selectedItem) => {
    setQuizItem(selectedItem); // 사용자가 선택한 항목 설정
    navigate("/quiz2");
  };

  return (
    <Wrapper>
      <Square src={square} alt='' />
      <Count>1/10</Count>
      <Progress value="10" min="0" max="100" />
      <QText>Q.1<br/>당신의 솔미 모에화는</QText>
      <SelectButton onClick={() => goQuiz('햄스터')}>햄스터 🐹</SelectButton>
      <SelectButton onClick={() => goQuiz('다람쥐')}>다람쥐 🐿️</SelectButton>
      <SelectButton onClick={() => goQuiz('고양이')}>고양이 🐈</SelectButton>
      <SelectButton onClick={() => goQuiz('강아지')}>강아지 🐶</SelectButton>
    </Wrapper>
  );
};

export default Quiz1;

 

4. Result 컴포넌트에서 QuizContext에서 선택한 항목을 가져와서 해당 항목에 맞는 결과를 보여준다

// Result.js
import React from 'react';
import { useQuizContext } from './QuizContext';
import { MainWrapper } from 'Styles/styles';
import result1 from 'Images/result/result1.svg';

const Result = () => {
  const { selectedItem } = useQuizContext();

  // 선택한 항목에 따라 결과 이미지 선택
  let backgroundImage;
  switch(selectedItem) {
    case '고양이':
      backgroundImage = result1;
      break;
    // 다른 항목에 대한 처리도 추가 가능
    default:
      backgroundImage = result1; // 기본값으로 설정
  }

  return (
    <MainWrapper style={{backgroundImage: `url(${backgroundImage})`}}>
      결과 페이지
    </MainWrapper>
  );
};

export default Result;

 

추가 문제점!!

퀴즈가 여러개 일 거 아냐! 그럼 selectItem이 여러개가 될텐데 만약에 고양이를 선택하고 또 다른 퀴즈에서 생머리를 선택하면 switch문은 어떻게 해야될까? selectItem을 1,2,3,4 이렇게 여러개 둬야할까?

 

해결책

→ 여러 개의 퀴즈가 있고 각 퀴즈마다 선택한 항목을 구분해야 한다면, 선택한 항목을 객체 형태로 저장하는 것이 좋다! 각 퀴즈마다 고유한 식별자를 키(key)로 사용하여 선택한 항목을 저장하면 된다. 이렇게 하면 selectedItem을 단일 값이 아니라 객체로 관리할 수 있음!

 

수정한 코드

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

const QuizContext = createContext();

export const useQuizContext = () => useContext(QuizContext);

export const QuizProvider = ({ children }) => {
  const [selectedItems, setSelectedItems] = useState({});

  const setQuizItem = (quizId, item) => {
    setSelectedItems(prevItems => ({
      ...prevItems,
      [quizId]: item
    }));
  };

  return (
    <QuizContext.Provider value={{ selectedItems, setQuizItem }}>
      {children}
    </QuizContext.Provider>
  );
};

 

⇒ 이렇게 하면 각 퀴즈의 선택 항목이 selectedItem 객체에 퀴즈 ID를 키로 사용하여 저장된다. 따라서 switch 문 대신에 선택한 항목을 퀴즈 ID를 통해 가져와서 처리할 수 있다.

 

Ex) selectedItem['quiz1']는 첫 번째 퀴즈에서 선택한 항목을 나타냄

selectedItem['quiz2']는 두 번째 퀴즈에서 선택한 항목을 나타냄

→ 선택한 항목을 가져올 때는 해당 퀴즈의 ID를 사용하여 가져오면 된다!

 

 

다음엔 이렇게 구현한 context API를 다른 상태 관리 툴을 사용해 바꿔 볼 예정이다.

recoil이나 react query 써보고 싶다!

 

근데 서버랑 데이터를 주고받는 상태도 아닌데 로컬에서만? 프론트에서만 상태관리를 하는 것도 공부에 도움이 되나 모르겠다...

개념 이해 + 어느정도 구현 + 적용은 공부가 되는데 실제로 써먹지는 못한다는 느낌?? ㅎ

적용을 시키는 프로젝트 규모가 너무 작아서 그런 건 맞는데 일단 개념 공부 + 구현 + 상태 관리 변경 시도 이걸로 만족해야겠다

요즘들어 큰 규모의 플젝을 하고싶다는 생각이 자주 든다

728x90