본문 바로가기
Today I Learned/SeSAC 웹 2기

React 상태관리 ContextAPI와 Redux | 13주차(1)하

by suyeonnie 2025. 1. 23.

 

📂 오늘 배운 내용

  • state의 종류와 상태관리(Context API, Redux)
  • js 함수: `Number.toLocaleString()` 회계 단위 표현
  • `ls -a` 숨겨진 파일(.git / .env) 포함 목록 조회 CLI

State 종류

  • Local State: 지금까지 사용한 상태들 useState
  • Cross-Component State: 지금까지 사용한 상태들. 2개 이상의 컴포넌트에서 props를 통해 상태 전달
  • App-wide state: 여러 컴포넌트 또는 앱 전체 영역에서 공유되어야 하는 데이터나 상태

>> corss-component, app-wide 일 때 context API 또는 Redux가 요구됨

 

전역 상태 관리의 필요성

 

  • 의미없는 행동(props drilling) 을 막기위해 전역 상태관리를 사용하게 됨
  • Props를 너무 많이 전달해줄 때 씀

 

리액트의 상태 관리 방법 :

1. 리액트 기본 기능 contextAPI `useContext`

2. 라이브러리 사용 (설치 필요): redux, recoil, react-query, mobX...


Context API 

context란? 컴포넌트 간 특정 값 공유 가능. State를 전역적으로 다룰 수 있음

 

# 기본 사용법

  • context는 Provider라는 컴포넌트를 제공함
  • value props로 넘겨준 값들을 하위컴포넌트 모두에서 사용 가능

 

1. createContext: 만들기 (context 폴더 따로 만들어서 Context 파일 관리)

 

2. provider: `App.js`에서 컴포넌트 감싸기

(1) `ThemContext` Context를 직접 Provider로 생성하고, valueProps로 전역 데이터 전달 

(2) `UserProvider` Provider 컴포넌트를 통해 Context 제공

 

[App.js]

App 컴포넌트는 상태 관리 책임을 가지지 않고, Provider 컴포넌트만 호출하여 Context 전달

 

 

[components/ Provider/ userProvider.js] 

import { useState } from 'react';
import { UserContext } from '../../context/UserContext';

// App.js state관리를 하는 것이 아닌
// 해당 state를 관리할 Provider를 따로 선언
export function UserProvider({ children }) {
  const [name, setName] = useState('');
  return (
    <UserContext.Provider value={(name, setName)}>
      {children}
    </UserContext.Provider>
  );
}

 

 

3. useContext: 실제로 사용하기 위해

 

[components/ Profile.jsx]

import { useContext, useRef } from 'react';
import { AgeContext } from '../context/AgeContext';
import { UserContext } from '../context/UserContext';

export default function Profile() {
  const ageContext = useContext(AgeContext); // app.js에서 가져오기
  const userContext = useContext(UserContext);
  
  console.log('ageContext', ageContext); //{age: 0, setAge: ƒ}
  console.log('userContext', userContext); //{name: '홍길동', setName: ƒ}

  const nameRef = useRef();
  const ageRef = useRef();
  console.log(nameRef.value);

  const { name, setName } = userContext;
  const { age, setAge } = ageContext;

  const changeInfo = () => {
    setAge(Number(ageRef.current.value));
    setName(nameRef.current.value);
  };

  return (
    <div>
      <h3>사용자 프로필</h3>
      <p>이름: &lt;context에서 값 가져오기 &gt;: {name}</p>
      <p>나이: &lt;context에서 값 가져오기 &gt;: {age}</p>

      <input type='text' placeholder='이름 입력' ref={nameRef} />
      <input type='number' placeholder='나이 입력' ref={ageRef} />
      <br />
      <button onClick={changeInfo}>변경</button>
    </div>
  );
}

 

# 폴더 구조

26.context-api/
├── node_modules/
├── public/
├── src/
│   ├── components/
│   │   ├── provider/
│   │   ├── Box.jsx
│   │   ├── Profile.jsx
│   ├── context/
│   │   ├── AgeContext.js
│   │   ├── ThemeContext.js
│   │   ├── UserContext.js
│   ├── pages/
│   │   ├── App.js
│   │   ├── index.js
├── .gitignore
├── App-before.js
├── App.js
├── package-lock.json
├── package.json
├── README.md

 

`Components`에서는 `Context`를 가져오고, `Context` 폴더에서 CreateContext로 만듦

 

 

# 주의(Official)

 

 

context 보다 컴포넌트 합성이 더 간단한 해결책일 수 있음. context사용하면 재사용이 어려워질 수 있기 때문!

 


Redux

 

# 용어 정리

useReducer와 비슷한 흐름으로 작동

1. store

  • 전역 상태 관리하는 저장소
  • 한 개의 프로젝트는 단 하나의 스토어만
  • 스토어 안에는 앱 상태와 리듀서가 들어있음
  • 스토어 데이터는 컴포넌트 직접 조작 x → 리듀서 함수로 관리

 

2. action

// action type 설정: 문자열 상수화
const CREATE = 'todo/CREATE';
const DONE = 'todo/DONE';

// action 생성 함수: 컴포넌트 내부에서 사용
export const create = (payload) => {
  return {
    type: CREATE,
    payload, // action과 함께 store로 전달되는 추가 데이터
  };
};
  • Action은 컴포넌트에서 Store로 데이터를 운반하는 객체. 데이터를 Store로 전달하기 위한 '택배상자'같은 역할
    • `type` : 무엇을 할 지, 리듀서 함수가 수행할 작업 명시
    • `payload` : 상태 업데이트 시 필요한 정보를 담는 선택 속성

 

3. dispatch

  • 액션 전달하는 함수
  • dispatch(action)과 같은 형태로 액션 객체를 인자로 넣어 호출

 

4. reducer

  • 액션의 타입에 따라 변화를 일으키는 함수 `reducer(현재 상태값, action 값){ 함수 }`
  • store에 저장된 State는 reducer 함수만이 변경할 수 있음
  • reducer는 기존 state를 변경하는 것이 아니라, 항상 새로운 state 객체 반환
  • http 요청, 데이터 저장 금지!!🧐

 

# 시작하기

 

모듈 설치

npm i redux react-redux @reduxjs/toolkit

 

@reduxjs/toolkit란?

- 리덕스 복잡성을 줄이기 위해 만들어진 도구

- 리덕스 관련 기능 효율적 구현(액션 생성, 리듀서, 미들웨어 등)

- `configureStore()` 리덕스 스토어 생성 함수

- `createSlice()` 리듀서 + 액션 생성 함수

 

 

# Redux 흐름과 폴더 구조

1. 흐름

2. 폴더 구조

27.react-redux/
├── node_modules/             
├── public/                   
├── src/                      
│   ├── components/           // React 컴포넌트 모음
│   │   ├── Bank.jsx          // 은행 상태 관리 및 기능 구현 컴포넌트

│   ├── store/                // Redux 상태 관리소
│   │   ├── module/           // 상태 관리 로직(리듀서 함수) 폴더
│   │   │   ├── bankReducer.js // 은행 상태 관리 리듀서 함수
│   │   ├── index.js          // 모든 리듀서를 통합(rootReducer)
│   ├── App.jsx               // 최상위 컴포넌트, 모든 컴포넌트를 포함하는 메인 컴포넌트
│   ├── index.js              // Store 설정 및 전달 (App.jsx 렌더링 엔트리 포인트)

├── .gitignore                
├── package-lock.json         
├── package.json              
├── README.md
  • store: 상태 관리소
  • store/module: 여러 개의 리듀서 함수 모아놓은 곳
  • store/ index.js : 여러 Reducer를 통합하는 역할

 

Redux 예제: 입출금 기능

1. src/ store/ module 개별적인 전역 state 선언 : reducer 생성

⭐️ reducer 선언 = state 만들기

 

실제로 상태를 관리하는 reducer 함수 만들기

 

@ src/ store/ module/ bankReducer.js

// action type의 상수화
const WITHDRAW = 'bank/WITHDRAW';
const DEPOSIT = 'bank/DEPOSIT';

// action 리턴 함수
export const deposit = (payload) => ({
  type: 'bank/DEPOSIT',
  payload: payload,
});
export const withdraw = (payload) => ({
  type: 'bank/WITHDRAW',
  payload,
});

const initialState = 0;

// [todo 1] src/store/module 개별적인 전역 state 선언 -> reducer 생성
export const bankReducer = (state = initialState, action) => {
  console.log('bank action', action);

  switch (action.type) {
    case DEPOSIT:
      return state + action.payload;
    case WITHDRAW:
      return state - action.payload;
    default:
      return state;
  }
};

 

2. store/ module에서 만들어준 여러개의 reducer 통합

 

@ src/ store/ index.js

// [todo2] store/module에서 만들어준 여러 개의 Reducer를 통합

// store/ index.js : 여러 개의 reducer를 통합하는 역할

import { combineReducers } from '@reduxjs/toolkit';
import { isDataReducer } from './module/isDataReducer';
import { counterReducer } from './module/counterReducer';
import { bankReducer } from './module/bankReducer';

const rootReducer = combineReducers({
  isData: isDataReducer,
  count: counterReducer,
  bank: bankReducer,
  // 만약 전역 관리 State가 추가되면 이곳에도 추가
  // Key값: useSelector를 이용해 가져오는 값
});

// [todo2-2] 내보내기 -> src/ index.js에서 쓰일 예정
export default rootReducer;

 

3. src/index.js 에서 store 설정하고 <Provider> 컴포넌트로 감싸기

 

@ src/ index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import rootReducer from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));

// [Todo3] store 설정
// module/ index.js에서 통합한 root reducer를 value로 전달
const store = configureStore({ reducer: rootReducer });

// [Todo4] App 컴포넌트의 자식 컴포넌트 모두에서 사용 가능하도록
// store Props로 store 전달
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

 

(1) `configureStore` 사용해서 Store 생성:

  • rootReducer 기반으로 Redux store 생성

 

(2) Redux 사용할 때는 Provider로 App을 감싸야함 :

  • `Provider` : React 애플리케이션의 최상위 컴포넌트를 Redux store에 연결하는 역할
  • `Provider` 컴포넌트는 `store`를 `props`로 받아 애플리케이션 전역에 전달
  • App 컴포넌트의 하위 컴포넌트 모두 redux store 접근 가능

 

4. 컴포넌트에서 사용하기

@ src/ components/ Bank.jsx 

import { useDispatch, useSelector } from 'react-redux';
import { deposit, withdraw } from '../store/module/bankReducer';
import { useState } from 'react';

export default function Bank() {
  const [inputNumber, setInputNumber] = useState(0);
  
  // [*] useSelector: Store에 저장되어 있는 state 가져오기
  const balance = useSelector((state) => state.bank);
  console.log('잔액', balance);

  // [*] useDispatch: action을 reducer로 전달하기 위한 dispatch 함수 제공
  const dispatch = useDispatch();
  
  return (
    <div>
      <p>잔액: {balance.toLocaleString()}원</p>
      <input
        type='number'
        step={10000}
        value={inputNumber}
        onChange={(e) => setInputNumber(Number(e.target.value))}
      />
      <button onClick={() => dispatch(deposit(inputNumber))}>입금</button>
      <button onClick={() => dispatch(withdraw(inputNumber))}>출금</button>
    </div>
  );
}
  • 상태 가져오기 `useSelector( )`
  • 상태 변경하기 `useDispatch( )`
  • dispatch의 action 타입에 따라 로직을 바꿔줄 수 있음

 

@ src/ App.jsx

import Bank from './components/Bank';

function App() {
  return (
    <>
      <h2>여러 개의 전역 state 사용하기</h2>
      <Bank />
    </>
  );
}

export default App;

 


useReducer vs ContextAPI vs Redux

차이점

구분 useReducer ContextAPI  Redux
용도 컴포넌트 내부 상태 관리 간단한 전역 상태관리,
Props Drilling 해결
전역 상태 관리 도구
범위 컴포넌트 단위 애플리케이션 전역 애플리케이션 전역
구조 - React Hook의 하나
- `useReducer(reducer, initial State)` 형태로 사용
- `Context.Provider`를 통해 상태를 전달하고, 'useContext`로 소비 - 상태를 관리하기 위해 `store`, `action`, `reducer` 등 설정해야함
데이터 흐름 상태와 로직이 컴포넌트에 국한됨 Provider를 통해 트리 구조의 하위 컴포넌트로 데이터 전달 상태와 로직이 전역적으로 적용
(상태가 중앙 저장소 store에 저장됨)
설치 여부 별도 설치 필요x
(React 내장 hook)
별도 설치 필요x
(React 내장 API)
`reudx`, `react-redux`등 패키지 설치 필요
미들웨어 x x 사용 가능: 비동기 작업 처리
(Redux-thunk, redux-saga 등)
디버깅 console.log console.log Redux DevTools 도구
특징 상태가 컴포넌트 내에 한정되어 있어 전역적 공유 어려움 Context 값을 많이 소비하면(빈번한 상태 업데이트 시) 렌더링 성능 저하 발생 가능 - 구조적으로 전역 상태를 효율적으로 관리하므로 대규모 애플리케이션에 적합

- 설정 및 초기 구성이 복잡함

 

 

 

 


💡 회고

 

1. Redux와 상태 흐름:

node.js sequelize 때 데이터 흐름을 따라가지 못했던 기분을 또 다시 느끼는 Redux 🤢

상태도 데이터라서 폴더와 파일들을 분리하고 관리해야 하는 점이 어렵게 느껴진다.

 

2. useReducer 과제 <to do list>를 구현할 때의 고민:

JSX가 복잡해지는 게 싫어서 처음에는 '할 일 추가, 삭제, 완료 표시 기능'을 모두 개별 이벤트 핸들러로 만들어서 onClick에 전달했었다. 작성하면서 reducer 함수와 ToDoApp 내의 이벤트 역할이 모호하게 느껴졌었다. Redux를 배우면서, useReducer에서 문자열 상수를 따로 관리했던 것처럼, Redux에서도 액션과 리듀서를  따로 관리할 수 있다는 걸 알게되었다!!!

 

근데 아직도 잘 모르겠다! 🤷‍♀️ 

 

3. HTML/EJS/JSX 태그와 가독성 문제:

ejs 때도 극혐이었지만 개인적으로 HTML/EJS/ JSX 태그에 뭔가 덕지 덕지(?) 붙어서 길어지는 게 싫다 🙃 특히 JSX에서는 태그가 복잡해질 수록 가독성이 떨어지는 것 같아서 뭔가...좀 더 깔끔해질 수 있는 방법이 없는지 궁금하다. 컴포넌트를 어떻게 나눠야할 지 모르겠음.

 

 


🌐  참고

서울시 청년취업사관학교 x 코딩온 웹 개발자 풀스택 과정 2기

26.context-api

- 상자 (themeContext)

- 사용자 프로필 정보 변경 기능

 

27.react-redux

- 상자들

- 은행 예금, 출금 기능