React

[ React ] 07. 상태관리 - context , redux

변쌤(이젠강남) 2024. 5. 23. 15:02
반응형

상태관리 context 와 redux

 

 

# 상태 관리를 사용하는 이유

 

1. 컴포넌트 간의 상태 공유

여러 컴포넌트가 동일한 데이터를 필요로 하는 경우, 상위 컴포넌트를 통해 상태를 전달하는 "prop drilling" 방식은 코드가 복잡해지고 관리하기 어려워집니다. 글로벌 상태 관리를 사용하면 상태를 중앙에서 관리하고 필요한 컴포넌트들이 직접 접근할 수 있어, 복잡성을 줄일 수 있습니다.

2. 복잡한 상태 관리의 단순화

글로벌 상태 관리 도구들은 상태를 효율적으로 관리하기 위한 다양한 패턴과 구조를 제공합니다. 예를 들어, Redux나 MobX 같은 라이브러리는 액션(action)과 리듀서(reducer)를 통해 상태를 예측 가능하고 체계적으로 관리할 수 있습니다. 이는 상태 변화의 원인을 명확하게 추적할 수 있게 해주며, 디버깅 및 유지보수를 용이하게 만듭니다.

3. 일관된 상태 관리

글로벌 상태 관리를 사용하면 애플리케이션 전역에서 상태의 일관성을 유지할 수 있습니다. 특히 대규모 애플리케이션에서 각 컴포넌트가 개별적으로 상태를 관리하면, 특정 상태가 다른 컴포넌트에서 다르게 보일 수 있는 문제(일관성 문제)가 발생할 수 있습니다. 글로벌 상태 관리는 이 문제를 해결하여 애플리케이션 전반에 걸쳐 일관된 상태를 유지하게 합니다.

4. 복잡한 사용자 상호작용 처리

사용자 인터페이스가 복잡해질수록 여러 컴포넌트 간의 상태를 동기화하고 일관되게 유지하는 것이 중요합니다. 예를 들어, 사용자가 로그인하면 여러 페이지나 컴포넌트에서 사용자의 상태를 즉시 반영해야 합니다. 글로벌 상태 관리 도구를 사용하면 이러한 상태 변경을 모든 컴포넌트에 자동으로 반영할 수 있습니다.

5. 서버 데이터 및 비동기 데이터 처리

서버에서 가져온 데이터를 여러 컴포넌트에서 동시에 사용할 때도 글로벌 상태 관리가 유용합니다. 예를 들어, React Query나 Recoil 같은 도구는 비동기 데이터를 중앙에서 관리하고, 그 상태가 여러 컴포넌트에서 자동으로 동기화되도록 돕습니다.

6. 성능 최적화

글로벌 상태 관리 도구들은 종종 상태 변경에 따른 불필요한 리렌더링을 방지하는 기능을 제공합니다. 예를 들어, Redux나 Recoil 같은 라이브러리는 상태가 변경될 때 관련된 컴포넌트만 업데이트되도록 하여 성능을 최적화할 수 있습니다.

7. 확장성

애플리케이션이 확장되면서 상태 관리가 복잡해질 수 있는데, 글로벌 상태 관리는 상태를 보다 체계적이고 확장 가능하게 관리할 수 있도록 설계되어 있습니다. 따라서, 프로젝트가 커질수록 코드베이스를 관리하기 쉽고 유지보수하기 용이합니다.

글로벌 상태 관리는 대규모 애플리케이션에서 상태를 중앙에서 관리하고, 상태의 일관성과 효율성을 보장하며, 코드의 유지보수성을 향상시키기 위해 사용됩니다.

 

 

# 로컬 상태 관리

  • useState: 컴포넌트 내에서 간단한 상태를 관리할 때 사용합니다. 리액트 훅으로 컴포넌트의 상태를 선언하고 업데이트할 수 있습니다.
  • useReducer: 복잡한 상태 로직이 필요한 경우, 리듀서를 통해 상태를 관리할 수 있습니다. Redux의 리듀서 패턴을 사용한 로컬 상태 관리입니다.

 

# 글로벌 상태관리  종류

  • Context API: 리액트의 내장 기능으로, 전역적으로 상태를 공유하고 싶을 때 사용합니다. 하지만 많은 양의 상태나 복잡한 로직을 처리할 때 성능 문제를 일으킬 수 있습니다.
  • Redux: 가장 널리 사용되는 글로벌 상태 관리 라이브러리입니다. 액션(action)과 리듀서(reducer)를 사용해 상태를 변경하고, 중앙 저장소(store)에 상태를 저장합니다. 상태의 예측 가능성을 높이고 디버깅이 용이합니다.
  • MobX: 상태가 변경될 때 자동으로 관련 컴포넌트가 리렌더링되도록 하는 옵저버 패턴 기반의 라이브러리입니다. 상대적으로 사용하기 간단하며, 선언적 접근을 지원합니다.
  • Recoil: 페이스북에서 만든 상태 관리 라이브러리로, 리액트의 비동기 및 파편화된 상태 관리를 개선하기 위해 설계되었습니다. Recoil은 Atom과 Selector라는 개념을 사용해 상태를 관리합니다.
  • Zustand: 상대적으로 가볍고 단순한 상태 관리 라이브러리입니다. 복잡한 설정이 필요 없고, 보다 쉽게 전역 상태를 관리할 수 있는 방법을 제공합니다.
  • Jotai: 또 다른 간단하고 확장 가능한 전역 상태 관리 라이브러리로, atom 단위로 상태를 관리합니다. 비동기 상태도 쉽게 관리할 수 있습니다.

 

 

1. context

 

React Context는 React 애플리케이션에서 데이터를 전역적으로 관리할 수 있게 해주는 기능입니다. 이를 통해 여러 컴포넌트 간에 데이터를 전달하거나 공유할 수 있습니다. 보통 props를 사용하여 부모 컴포넌트로부터 자식 컴포넌트로 데이터를 전달하는데, 이러한 경우 컴포넌트 트리의 중간에 있는 컴포넌트들은 데이터를 전달하기 위해 필요하지 않은 props를 받게 될 수 있습니다. 이런 경우 Context를 사용하면 중간에 있는 컴포넌트들을 건너뛰고 데이터를 바로 필요한 컴포넌트로 전달할 수 있습니다.

 

 

Provider는 데이터를 제공하고 . Provider는 하위 컴포넌트에게 전역적인 데이터를 전달하는 데 사용

보통 Context를 사용하는 곳은 언어 설정, 사용자 인증 상태, 테마, 혹은 애플리케이션 전역적인 상태와 설정과 같은 데이터를 전역적으로 관리해야 하는 경우입니다. 이러한 데이터는 여러 컴포넌트에서 사용되기 때문에 Context를 사용하여 중앙 집중식으로 관리하는 것이 효율적입니다.

 

 

 

 

https://ko.reactjs.org/docs/context.html

 

 

 

https://ko.legacy.reactjs.org/docs/context.html#gatsby-focus-wrapper

 

Context – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

 

https://react.dev/learn/scaling-up-with-reducer-and-context

 

Scaling Up with Reducer and Context – React

The library for web and native user interfaces

react.dev

 

 

컴포넌트 props 값 전달 

props

 

 

context  형식 

 

context

 

 

1. UI 디자인

const 디자인컴포넌트 = () => {    
    return (
        <div>
            <h2>  UI 디자인  </h2>            
            <p>            
               출력 내용
            </p>
        </div>
    );
};

export default 디자인컴포넌트;

 

 

 

2. Context 

import { createContext, useState } from "react";

export const 관리자 = createContext()

const 메세지컴포넌트 = ( props ) => {
    const [ msg , setMsg ] = useState('테스트값')
    return (
        <관리자.Provider value={{ 넘겨줄값 }}>
            { props.children}
        </관리자.Provider>
    );
};

export default 메세지컴포넌트;

 

 

3. App.jsx : Context 에서 디자인을 연결 

<메세지컴포넌트>
   <디자인컴포넌트 />
</메세지컴포넌트>

 

 

4. UI 컴포넌트 

 

import { useContext } from "react";
import { 관리자 } from "../메세지컴포넌트경로";

const 디자인컴포넌트 = () => {
    const { 상태변수 } = useContext(관리자)
    return (
        <div>                    
            <p>
              출력 메세지 : { 상태변수 }
            </p>
        </div>
    );
};

export default 디자인컴포넌트;

 

 

useContext를 사용하면 Context에 저장된 값을 가져와서 해당 컴포넌트에서 사용할 수 있습니다.

일반적으로, Context를 생성한 후 useContext 훅을 사용하여 해당 Context를 구독(subscribe)하고 값을 가져올 수 있습니다. 이를 통해 컴포넌트에서 전역 상태나 설정과 같은 데이터에 접근할 수 있습니다.

 

 

예) 

 

UI디자인

import React, { useContext } from 'react';

// 자식 컴포넌트에서 언어 설정을 사용하는 예시
function MyComponent() {
  const language = useContext(LanguageContext);

  return <p>현재 언어는: {language}</p>;
}

 

 

Context 

// 언어 설정을 저장하는 Context 생성
const LanguageContext = React.createContext();

// 부모 컴포넌트에서 언어 설정을 제공하는 Provider
function LanguageProvider({ children }) {
  const language = 'en'; // 예시로 'en'을 기본값으로 설정
  return (
    <LanguageContext.Provider value={language}>
      {children}
    </LanguageContext.Provider>
  );
}

 

App 연결 

// 언어 설정을 사용하는 부모 컴포넌트를 렌더링
function App() {
  return (
    <LanguageProvider>
      <MyComponent />
    </LanguageProvider>
  );
}

export default App;

 

 

 

2.  redux

 

React ReduxReactRedux를 함께 사용하는 패턴 또는 라이브러리

React는 컴포넌트 기반의 UI 라이브러리이고, Redux는 상태 관리를 위한 라이브러리입니다. 이 두 가지를 결합하면 대규모 애플리케이션에서 복잡한 상태를 효율적으로 관리할 수 있습니다.

 

 

store 모두 한 곳에서 집중 관리

상태는 불변(읽기 전용) 데이터 이며, 오직 액션 만이 상태 교체를 요청 할 수 있음

리듀서(함수)를 통해 상태의 최종 값만 설정

무엇이 일어나는지는 dispatch를 이용해서 알리며 어떻게 바꿀지는 reducer를 이용해서 state를 조작

 

retux

 

 

  1. 전역 상태 관리: React의 기본적인 상태 관리는 컴포넌트 간 데이터 전달을 위해 props를 사용합니다. 그러나 애플리케이션이 복잡해지면 상태를 관리해야 하는 컴포넌트의 수가 늘어나게 되는데, 이를 전역적으로 관리하기 위해 React Redux를 사용합니다. Redux 스토어는 애플리케이션 전체에서 사용 가능한 단일 상태 트리를 제공하여 상태를 효율적으로 관리할 수 있습니다.
  2. 단방향 데이터 흐름: Redux는 단방향 데이터 흐름을 따릅니다. 이는 애플리케이션의 상태가 예측 가능하고 디버그하기 쉬워진다는 장점이 있습니다. 상태 변경은 명시적으로 액션을 디스패치하여 이뤄지며, 이는 Redux의 불변성 원칙을 따르기 때문에 상태 변화를 추적하기 쉽습니다.
  3. 컴포넌트 간 결합도 감소: Redux를 사용하면 상태 관리를 위한 로직이 컴포넌트에서 분리됩니다. 이로 인해 컴포넌트가 더 간단해지고 재사용성이 높아집니다. 또한 컴포넌트는 상태를 직접 관리하는 대신 Redux 스토어와 상호작용하여 필요한 데이터를 가져올 수 있습니다.
  4. 개발자 도구와의 통합: Redux는 강력한 개발자 도구와의 통합을 제공합니다. Redux DevTools를 사용하면 상태의 변경 이력을 시간에 따라 추적하고 디버깅할 수 있습니다. 또한 애플리케이션의 상태를 실시간으로 모니터링하고 효율적으로 테스트할 수 있습니다.
  5. 서버 사이드 렌더링 및 테스트 용이성: Redux는 서버 사이드 렌더링과 테스트에 용이합니다. 애플리케이션의 상태가 예측 가능하고 순수한 함수로 상태 변경이 이뤄지기 때문에 테스트 작성이 간편하고 안정적입니다. 또한 서버 사이드 렌더링을 쉽게 구현할 수 있어 초기 로딩 시간을 최적화할 수 있습니다.

 

 

redux

 

 


 

# Redux Toolkit 사용

 

Redux에서는 액션 타입, 액션 생성자, 리듀서를 각각 작성해야 하지만

Redux Toolkit에서는 이 모든 것을 한 번에 처리할 수 있습니다.

 

https://redux-toolkit.js.org/

 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

 

 

https://redux-toolkit.js.org/introduction/getting-started

 

Getting Started | Redux Toolkit

 

redux-toolkit.js.org

 

 

설치

 

yarn add @reduxjs/toolkit

yarn add react-redux

 

 

https://redux-toolkit.js.org/tutorials/quick-start

 

Quick Start | Redux Toolkit

 

redux-toolkit.js.org

 

 

전통적인 Redux에서는 액션(Action), 액션 생성자(Action Creator), 
리듀서(Reducer) 를 각각 작성해야 하지만 Redux Toolkit을 사용하면 createSlice 함수로 액션과 리듀서를 한 번에 생성할 수 있고    createSlice는 리듀서와 액션을 자동으로 생성하고, 상태 변경 로직을 간결하게 작성할 수 있게 도와줍니다.

 

Redux Toolkit은 단순성, 자동화된 상태 관리, 비동기 작업의 간소화, 보일러플레이트( 반복적이고 기본적인 작업을 수행하기 위해 거의 동일하게 작성되는 코드 ) 감소 등 다양한 장점을 제공하며, 전통적인 Redux의 복잡성을 크게 줄여주고 특히 비동기 처리, 스토어 설정, 미들웨어 관리가 자동화되어 대규모 애플리케이션 개발에서도 매우 유용하게 활용될 수 있습니다.

 

 

Redux Toolkit은 Immer(불변성) 라이브러리를 내장하여 리듀서에서 직접 상태를 변경하는 것처럼 보이지만, 내부적으로는 불변성을 유지하며 상태를 안전하게 업데이트할 수 있습니다. 개발자는 상태를 직접 수정하는 것처럼 코드를 작성할 수 있어 편리합니다.

 

 

createAsyncThunk 함수를 사용하면 비동기 액션을 간편하게 생성할 수 있으며, 요청 상태(로딩, 성공, 실패)를 자동으로 관리합니다.

 

const fetchUser = createAsyncThunk('users/fetchUser', async (userId, thunkAPI) => {
  const response = await fetch(`/api/user/${userId}`);
  return response.json();
});

 

configureStore 함수는 Redux 스토어 설정을 간소화해줍니다. Redux Toolkit은 기본적으로 미들웨어를 내장하고 있으며, 개발용 도구(DevTools) 설정과 함께 Redux Thunk와 같은 미들웨어도 자동으로 포함됩니다.

 

const store = configureStore({
  reducer: {
    키: reducer,
  },
});

 

 

createAsyncThunk를 사용하면 비동기 작업을 매우 쉽게 처리할 수 있습니다. 이를 통해 로딩 상태와 에러 상태를 관리할 수 있으며, 별도의 액션 생성자나 리듀서 작성 없이 비동기 로직을 관리할 수 있습니다.

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    list: [],
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.list.push(action.payload);
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

 

 

 


 

 

 

순서

1. 화면에 보일 UI만들기
2. 리듀서 만들기
3. 리듀서 합치기
4. main.js에 스토어 생성해서 3번 리듀서 자식(자손)컴포넌트에 전달하기
5. 원하는 UI에 액션 ( useDispatch ) , state 연결해서 사용하기 ( useSelector )


1. UI 디자인

const 디자인컴포넌트 = () => {    
    return (
        <div>
            <h2>  UI 디자인  </h2>            
            <p>            
               출력 내용
            </p>
        </div>
    );
};

export default 디자인컴포넌트;

 

 

2.  공식문서 예제

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {      
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})
 
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

 

createSlice 함수는 리듀서를 생성하는 데 사용되며, 초기 상태와 리듀서 함수들을 정의할 수 있습니다. 이를 통해 Redux 상태를 관리하는데 필요한 액션 타입과 액션 생성자를 자동으로 생성해줍니다.

 

초기 상태로 { value: 0 }를 설정하고, increment, decrement, incrementByAmount와 같은 액션 생성자들을 정의합니다. 이 액션 생성자들은 각각 상태를 증가시키는, 감소시키는, 또는 특정 값만큼 증가시키는 액션들을 생성합니다.

 

Redux Toolkit은 불변성을 유지하면서 상태를 업데이트할 수 있도록 Immer 라이브러리를 사용합니다. 따라서 위의 리듀서 함수들에서는 직접적으로 상태를 변경할 수 있습니다. 이 변경사항은 Immer 라이브러리에 의해 감지되고 새로운 불변성을 가진 상태를 생성합니다.

 

마지막으로, counterSlice.reducer는 생성된 리듀서 함수를 내보내며, counterSlice.actions는 생성된 액션 생성자들을 내보냅니다. 이를 통해 Redux 스토어에 이 리듀서를 등록하고 액션을 디스패치하여 상태를 업데이트할 수 있습니다.

 

2. Redux Toolkit 

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  초기값설정
}

export const slice명 = createSlice({
  name: '이름',
  initialState,
  reducers: {
    함수명: (state, action) => {      
      
    },
    함수명: (state, action) => {      
      
    }
  },
})
 
export const { 함수명 } = slice명.actions
export default slice명.reducer

 

 

3. 리듀서 합치기 

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {},
})

 

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

 

 

4. main.jsx 

스토어 생성해서 3번 리듀서 자식(자손)컴포넌트에 전달하기

import { store } from './경로/store'
import { Provider } from 'react-redux'



<Provider store={store}>
    <App />
</Provider>

 

 

5. UI 디자인 연결 : 

  
  const count = useSelector((state) => state.counter.value) : 값 처리  
  const dispatch = useDispatch() : 액션처리

 

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'

export function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

 

Redux의 useSelector 훅을 사용하여 상태를 가져오고, useDispatch 훅을 사용하여 액션을 디스패치하는 방법을 보여주는 React 함수형 컴포넌트입니다.

  1. useSelector: 이 훅은 Redux 스토어의 상태를 가져오는 데 사용됩니다. 여기서는 state.counter.value를 통해 Redux 스토어의 counter 슬라이스의 value 속성을 가져옵니다. 따라서 count 변수에는 현재 카운터의 값이 할당됩니다.
  2. useDispatch: 이 훅은 Redux 스토어로부터 dispatch 함수를 가져옵니다. dispatch 함수를 사용하여 액션을 디스패치하여 상태를 업데이트할 수 있습니다.

함수형 컴포넌트인 Counter는 카운터를 표시하고 증가 또는 감소하는 버튼을 렌더링합니다. Increment 버튼을 클릭하면 increment 액션을 디스패치하고, Decrement 버튼을 클릭하면 decrement 액션을 디스패치합니다.

 

디스패치는 액션 객체를 전달하여 스토어에 상태 변경을 알리는 역할

 

 

 

https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko

 

Redux DevTools

Redux DevTools for debugging application's state changes.

chromewebstore.google.com

 

 

 


 

 

 

# 비동기 : createAsyncThunk

 

 

https://redux-toolkit.js.org/api/createAsyncThunk

 

createAsyncThunk | Redux Toolkit

 

redux-toolkit.js.org

 

 

createAsyncThunk는 비동기 작업을 간단하고 효율적으로 처리하기 위해 제공하는 유틸리티 함수입니다.

createAsyncThunk로 생성한 비동기 작업을 처리할 리듀서를 정의합니다. 비동기 작업의 상태에 따라 다른 액션을 처리합니다 (pending, fulfilled, rejected).

 

import { createAsyncThunk } from '@reduxjs/toolkit';

// 사용자 데이터를 가져오는 비동기 작업
export const fetchUserData = createAsyncThunk(
  'users/fetchUserData',
  async (userId, thunkAPI) => {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    return data;
  }
);

 

 

import { createSlice } from '@reduxjs/toolkit';
import { fetchUserData } from './userThunks';

const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    status: 'idle',
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserData.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchUserData.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.data = action.payload;
      })
      .addCase(fetchUserData.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export default userSlice.reducer;

 

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUserData } from './userThunks';

const UserProfile = ({ userId }) => {
  const dispatch = useDispatch();
  const user = useSelector((state) => state.user.data);
  const status = useSelector((state) => state.user.status);
  const error = useSelector((state) => state.user.error);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUserData(userId));
    }
  }, [status, dispatch, userId]);

  if (status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'failed') {
    return <div>Error: {error}</div>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

export default UserProfile;

 

 

 

 

반응형