개발자 Saaad

[React] Redux 상태 관리 (redux-counterApp을 통해 실습) 본문

학습/kakao X goorm 풀스택12회차

[React] Redux 상태 관리 (redux-counterApp을 통해 실습)

Saaad 2024. 12. 16. 21:36

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

 

Getting Started with Redux | Redux

Introduction > Getting Started: Resources to get started learning and using Redux

redux.js.org

 

안녕하세요 오늘은 React의 Redux에 대해서 간단하게 코드를 통해 알아보는 시간을 가져보겠습니다. 

 

 

Redux란 ?

Redux는 예측 가능하고 유지 관리 가능한 전역 상태 관리를 위한 JS 라이브러리입니다.

일관되게 동작하고, 다양한 환경(클라이언트, 서버, 네이티브)에서 실행되며, 테스트하기 쉬운 애플리케이션을 작성하는 데 도움이 됩니다.

 

상태 관리라는 점에서 React의 Hooks 중 useState() 를 떠올리실 수 있는데요. 

상태변수를 전역적으로 관리할 수 있다는 점에서 좀 더 고급(?) 기술이라고 할 수 있을 것 같습니다. 

 

그리고 설치하게 될 Redux ToolKitRedux 코어를 감싸고 있으며, Redux 앱을 빌드하는 데 필수적이라고 생각되는 패키지와 함수를 포함합니다. Redux Toolkit은 제안된 모범 사례를 빌드하고, 대부분의 Redux 작업을 간소화하고, 일반적인 실수를 방지하며, Redux 애플리케이션을 작성하기 쉽게 만듭니다.

 

Redux ToolKit은 다음과 같이 패키지로 제공됩니다.

# NPM 
npm install @reduxjs/toolkit

# Redux 코어 라이브러리 설치
npm install redux

# Yarn
yarn add @reduxjs/toolkit

 


 

 

이제 간단한 카운터 앱을 직접 만들면서 Redux 실습을 바로 해보겠습니다.


시작

일단 카운터 앱을 만들 프로젝트를 생성해줍니다.

mkdir redux-counter-app

cd redux-counter-app

//현재 디렉토리에 프로젝트 설치
npx create-react-app ./

 

일정 시간이 지난 후에 아래와 같은 구조로 파일들이 생성될 것입니다.

 

디렉토리 구조

이제 카운터 앱을 만들기 위해 기존의 필요없는 코드는 지워줍시다.

App.css의 내용은 전부 지워도 괜찮습니다.

 

App.js

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div>
      하이하이
    </div>
  );
}

export default App;

 

그리고 우리가 만들 카운터 앱의 구조를 간단하게 작성해봅시다.

 

App.js

import logo from './logo.svg';
import './App.css';

function App() {
    return (
        <div className="container">
            <h1>counter : {}</h1>
            <div className="buttons">
            <button>Increment</button>
                <button>Decrement</button>
                <button>Reset</button>
            </div>
        </div>
    );
}

export default App;

 

 

App.css

.container {
  margin: 0 auto;
  font-size: 20px;
}

button {
  font-size: 20px;
}

 

 

실행

위와 같이 아주 간단한 레이아웃으로 만들어보았습니다. 

 


이제 Redux를 이용해서 counter에 출력될 숫자의 상태를 관리해주도록 할 것인데요. 

그 전에 useState()를 이용해서 상태를 변경하는 예시를 먼저 살펴보겠습니다. 

 

import logo from './logo.svg';
import './App.css';
import {useState} from "react";

function App() {
    const [counter, setCounter] = useState(0);

    const handleCounter = (action) => {
        if (action === 'increment'){
            setCounter(counter + 1);
        }
        if (action === 'decrement'){
            setCounter(counter - 1);
        }
        if (action === 'reset'){
            setCounter(0);
        }
    }
    return (
        <div className="container">
            <h1>counter : {counter}</h1>
            <div className="buttons">
                <button onClick={() => handleCounter('increment')}>Increment</button>
                <button onClick={() => handleCounter('decrement')}>Decrement</button>
                <button onClick={() => handleCounter('reset')}>Reset</button>
            </div>
        </div>
    );
}

export default App;

 

위 코드를 살펴보면 useState 훅을 이용해 초기값을 0으로 초기화 하고, handleCounter 함수를 이용해서 setCounter를 통해

상태 변수를 컨트롤하는 모습을 확인할 수 있습니다. 

위와 같이 동작도 잘 하는 모습을 확인할 수 있습니다.

 

useState 훅을 사용하면 간단하게 상태변수를 관리할 수 있다는 장점이 있는데요,

만약에 이 상태변수를 자식 컴포넌트에게 넘겨줘야한다거나, 또는 전역적으로 관리해야한다면 어떻게 해야할까요 ? 

 

 

자식 컴포넌트를 하나 생성해주고 아래와 같이 작성해주었습니다

 

childComponent.js

import React from 'react';

const ChildComponent = ({counter}) => {
    return (
        <div>
            <span> counter : {counter}</span>
        </div>
    );
};

export default ChildComponent;

 

props를 통해 받은 counter를 출력하는 간단한 컴포넌트입니다.

 

그렇다면 props를 제공하는 코드 또한 작성해주어야겠죠 ?

 

App.js

import ChildComponent from "./component/childComponent";

function App() {
    ...
    return (
        <div className="container">
            <h1>counter : {counter}</h1>
            <div className="buttons">
                <button onClick={() => handleCounter('increment')}>Increment</button>
                <button onClick={() => handleCounter('decrement')}>Decrement</button>
                <button onClick={() => handleCounter('reset')}>Reset</button>
            </div>
            
            <ChildComponent counter={counter}/>
        </div>
    );
}

export default App;

위 코드와 같이 ChildComponent를 불러오고, props를 통해 상태 변수 값을 전달해주면 됩니다.

<ChildComponent counter={counter}/>

 

그럼 아래와 같이 동작하겠죠

 

아래 카운터는 childComponent에서 렌더링 된 값으로 똑같이 counter값이 변함에 따라 

함께 변하는 모습을 볼 수 있습니다.

 

그렇다면 만약 자식 컴포넌트에서 상태변수를 변경하고 싶다면 .... 

props를 이용해 자식컴포넌트에게 hadleCounter 함수를 제공해주면 됩니다.

똑같이 버튼을 생성해주고 .. 말이죠 .

 

근데 이 예제에서는 그렇게 큰 문제가 되지 않습니다. 

만약에 자식 컴포넌트가 매우 많아지고, 그 자식들이 모두 상태를 변경하기를 원하면 어떻게 될까요??

 

이 때 Props driling 문제가 발생합니다.


Props driling?

출처 : https://medium.com/@omkarbhavare2406/prop-drilling-in-react-8819c609c376

 

위 사진과 같이 App 컴포넌트에서 props로 제공하는 name, email을 자식 컴포넌트에게 내려주기 위해

App -> component1 -> component2 -> ... 

와 같이 계속해서 자식에게 props를 중첩해서 전달하는 문제를 props driling 이라고 합니다.

또한 중간 컴포넌트가 이 값을 필요로 하지 않지만 자식이 필요해서 props를 전달해야하는 경우도 생깁니다.

 

이렇게 작성했을 때 발생하는 문제점은 다음과 같습니다.

 

문제점

 

  1. 프로퍼티 데이터 형식 변경의 불편함:
    Prop drilling 데이터의 데이터 형식을 변경해야 하는 경우, 컴포넌트 계층 전체에서 업데이트하는 것이 어려울 수 있습니다.
  2. 중간 컴포넌트에 불필요한 프로퍼티 전달:
    컴포넌트 분리 과정에서 중간 컴포넌트를 통해 불필요한 프로퍼티가 전달될 수 있어 불필요한 복잡성을 초래할 수 있습니다.
  3. 누락된 프로퍼티 인지의 어려움:
    필요한 프로퍼티가 타겟 컴포넌트에 전달되지 않은 상황을 인지하기 어려울 수 있어 잠재적인 문제를 발견하기 어려울 수 있습니다.
  4. 프로퍼티 이름 변경 추적의 어려움:
    프로퍼티 이름이 계층에서 변경되면 해당 값을 추적하고 업데이트하는 것이 어려워질 수 있습니다.

 

해결법 

  1. Context API:
        React의 Context API를 사용하여 데이터를 전역적으로 공유할 수 있습니다. Context를 생성하고 값을 제공하는 컴포넌트를 작성한 다음, 필요한 컴포넌트에서 useContext 훅을 사용하여 해당 값을 직접 접근할 수 있습니다.
        이를 통해 중간 컴포넌트를 거치지 않고도 데이터를 전달할 수 있습니다.
  2. Redux 또는 다른 상태 관리 라이브러리:
        Redux와 같은 상태 관리 라이브러리를 사용하면 애플리케이션의 상태를 중앙에서 관리할 수 있습니다. 상태를 저장하고 필요한 컴포넌트에서 상태를 가져와 사용할 수 있습니다.
        이를 통해 prop drilling을 피하고 상태를 전역적으로 공유할 수 있습니다.
  3.  Custom Hooks:
        Custom Hooks를 사용하여 관련된 로직을 재사용 가능한 함수로 추상화할 수 있습니다. 커스텀 훅 내에서 상태와 로직을 처리하         고, 필요한 컴포넌트에서 해당 훅을 호출하여 데이터를 가져올 수 있습니다.
        이를 통해 prop drilling을 해소하고 데이터 전달을 보다 간편하게 할 수 있습니다.
  4. Render Props 패턴과 Children props:
        Render Props 패턴이나 Children props를 활용하여 데이터를 부모 컴포넌트에서 자식 컴포넌트로 전달할 수 있습니다.
        Render Props 패턴은 부모 컴포넌트에서 함수를 정의하고, 
        자식 컴포넌트에서 해당 함수를 호출하여 데이터를 전달받을 수 있습니다.
        Children props는 부모 컴포넌트에서 컴포넌트 태그 사이의 내용을 자식 컴포넌트로 전달합니다.

출처 :https://velog.io/@rachel28/Prop-Drilling


Redux 적용!! 

이제 Redux 를 이용해 상태를 관리해보고, 원하는 곳에서 상태 변수나 상태변수를 변경하는 함수를 호출해보도록 하겠습니다.

기존에 작성했던 useState() 훅이나 핸들러함수는 제거해주고 다시 시작해보겠습니다.

 

 

그리고 루트 폴더에 다음과 같이 reducer와 store 폴더를 만들어주고 그 안에 index.js 파일을 생성해주도록 하겠습니다.

 

/store/index.js

import {createStore} from '@reduxjs/toolkit'
import rootReducer from '../reducer'

const store = createStore(rootReducer);

export default store;

 

 

createStore를 패키지로부터 가져오고, rootReducer는 아래 작성한 파일로부터 가져오도록 합니다.

 

그런 후 store 변수를 선언하고 rootReducer를 인자로 받게 하여 createStore() 를 통해 store에 담아봅니다.

 

 

/reducer/index.js

const initialState = {
    counter: 0
}

const rootReducer = (state = initialState, action) => {
    switch (action.type) {
        case "INCREMENT":
            return {
                ...state,
                counter: state.counter + 1
            }
        case "DECREMENT":
            return {
                ...state,
                counter: state.counter - 1
            }
        case "RESET":
            return {
                ...state,
                counter: 0
            }
        default:
            return state;
    }
}

export default rootReducer;

 

initialState 는 관리할 상태변수들을 담는 공간입니다.

지금은 counter 상태 하나를 담았지만 필요하다면 다음과 같이 다른 상태변수도 담을 수 있습니다.

const initialState = {
    state : {
        counter: 0,
        name: "test"
    }
}

 

그리고 rootReducer 를 통해 상태 변화에 맞춰 수행할 동작을 정의해줄 수 있습니다.

switch-case 문을 통해서 상태를 변경시킵니다.

 

그리고 default :  부분은 까먹지 않고 꼭 작성해주시길 바랍니다.

제가 요거땜에 시간을 좀 잡아먹었어서.. ㅎ 

 


전역에서 사용할 수 있게 Provider 사용!!

아무튼 이렇게 두개의 파일을 작성해주고 이제 상태 변수를 불러와서 사용해봐야 합니다.

그러기 위해서 Provider를 이용해서 루트에서 모두 사용할 수 있게 뿌려주도록 하겠습니다.

 

index.js (root 디렉토리에 있어요)

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import {Provider} from "react-redux";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
      <Provider store={store}>
          <App />
      </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals

react-redux 패키지의 Provider를 이용해서 store를 App컴포넌트 전역에서 사용할 수 있도록 props로 제공해줍니다.

반드시 <Provider > 안에 App 컴포넌트가 종속될 수 있도록 해줍시다.

 

 


컴포넌트에서 redux 상태변수를 불러오고, 상태를 변경하는 방법

이제 상태변수를 사용할 준비가 되었으니 본격적으로 값을 이용해봅시다.

 

 


상태변수 접근

이때 "useSelector()" 메소드를 이용합니다

 

useSelector를 이용하면 상태 변수에 접근할 수 있습니다.

이번 예제의 경우 reducer의 initialState의 counter에 접근하기 위해 state.counter 로 호출하였습니다.

 

App.js

import {useDispatch, useSelector} from "react-redux";

function App() {
	const counter = useSelector(state => state.counter)
   ...   
   
   return(
   ...
   );
}

 

 

 

만약 아까처럼 다른 상태변수가 있어서 아래처럼 state에 담았다면,

const initialState = {
    state : {
        counter: 0,
        name: "test"
    }
}

 

아래처럼 접근할 수 있겠습니다.

const counter = useSelector(state => state.state.counter)
const name = useSelector(state => state.state.name)

 


상태변수의 상태를 변경

상태 변수를 불러왔다면, 상태변수를 변경할 수 있는 함수를 사용해봐야겠죠.

아주 간단하게 "useDispatch()" 를 사용해 상태를 변경할 수 있습니다.

아래와 같이 변수를 선언하면 됩니다.

const dispatch = useDispatch();

 

이제 dispatch를 이용해서 핸들러를 만들어볼건데요,

useState의 setter를 사용하는 것과 유사하게 작성해보겠습니다.

 

아까 reducer 코드를 작성할 때 action.type의 case에 해당하는 인자들을 값으로 주면 됩니다.

각각의 핸들러를 dom컨테이너에서 onClick()메소드를 이용해 호출해주면 되겠습니다.

const increment = () => {
        dispatch({type: "INCREMENT"})
    }
    const decrement = () => {
        dispatch({type: "DECREMENT"})
    }
    const reset = () => {
        dispatch({type: "RESET"})
    }

 

 

 

최종 App.js 코드

import logo from './logo.svg';
import './App.css';
import ChildComponent from "./component/childComponent";
import {} from "@reduxjs/toolkit/query/react";
import {useDispatch, useSelector} from "react-redux";


function App() {
    const counter = useSelector(state => state.counter)

    const dispatch = useDispatch();

    const increment = () => {
        dispatch({type: "INCREMENT"})
    }
    const decrement = () => {
        dispatch({type: "DECREMENT"})
    }
    const reset = () => {
        dispatch({type: "RESET"})
    }

    return (
        <div className="container">
            <h1>counter : {counter}</h1>
            <div className="buttons">
                <button onClick={() => increment()}>Increment</button>
                <button onClick={() => decrement()} >Decrement</button>
                <button onClick={() => reset()} >Reset</button>
            </div>

        </div>
    );
}

export default App;

 

 

실행결과


지금까지 React의 redux에 대해서 알아보았습니다.

redux를 이용하면 props driling 문제를 해결할 수 있고, 전역적으로 사용하는 상태변수를 손쉽게 관리할 수 있고

추적이 용이하다는 것을 알 수 있었습니다.

처음 접하면 어려운 개념이지만 코드의 길이가 간결해지고 가독성이 높아지는 장점이 있으므로 공부해서 적용해보면 좋을 것 같습니다.

 

아래는 제가 redux를 공부할 때 참고했던 유튜브 영상입니다. 

영상은 제가 다룬 포스팅과 비슷한 양상으로 진행됩니다.

https://youtu.be/HxutXYq3T8U?si=lfy8Tb5HT_SbuUW-