개발자 Saaad
[React] Redux 상태 관리 (redux-counterApp을 통해 실습) 본문
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 ToolKit은 Redux 코어를 감싸고 있으며, 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?
위 사진과 같이 App 컴포넌트에서 props로 제공하는 name, email을 자식 컴포넌트에게 내려주기 위해
App -> component1 -> component2 -> ...
와 같이 계속해서 자식에게 props를 중첩해서 전달하는 문제를 props driling 이라고 합니다.
또한 중간 컴포넌트가 이 값을 필요로 하지 않지만 자식이 필요해서 props를 전달해야하는 경우도 생깁니다.
이렇게 작성했을 때 발생하는 문제점은 다음과 같습니다.
문제점
- 프로퍼티 데이터 형식 변경의 불편함:
Prop drilling 데이터의 데이터 형식을 변경해야 하는 경우, 컴포넌트 계층 전체에서 업데이트하는 것이 어려울 수 있습니다. - 중간 컴포넌트에 불필요한 프로퍼티 전달:
컴포넌트 분리 과정에서 중간 컴포넌트를 통해 불필요한 프로퍼티가 전달될 수 있어 불필요한 복잡성을 초래할 수 있습니다. - 누락된 프로퍼티 인지의 어려움:
필요한 프로퍼티가 타겟 컴포넌트에 전달되지 않은 상황을 인지하기 어려울 수 있어 잠재적인 문제를 발견하기 어려울 수 있습니다. - 프로퍼티 이름 변경 추적의 어려움:
프로퍼티 이름이 계층에서 변경되면 해당 값을 추적하고 업데이트하는 것이 어려워질 수 있습니다.
해결법
- Context API:
React의 Context API를 사용하여 데이터를 전역적으로 공유할 수 있습니다. Context를 생성하고 값을 제공하는 컴포넌트를 작성한 다음, 필요한 컴포넌트에서 useContext 훅을 사용하여 해당 값을 직접 접근할 수 있습니다.
이를 통해 중간 컴포넌트를 거치지 않고도 데이터를 전달할 수 있습니다. - Redux 또는 다른 상태 관리 라이브러리:
Redux와 같은 상태 관리 라이브러리를 사용하면 애플리케이션의 상태를 중앙에서 관리할 수 있습니다. 상태를 저장하고 필요한 컴포넌트에서 상태를 가져와 사용할 수 있습니다.
이를 통해 prop drilling을 피하고 상태를 전역적으로 공유할 수 있습니다. - Custom Hooks:
Custom Hooks를 사용하여 관련된 로직을 재사용 가능한 함수로 추상화할 수 있습니다. 커스텀 훅 내에서 상태와 로직을 처리하 고, 필요한 컴포넌트에서 해당 훅을 호출하여 데이터를 가져올 수 있습니다.
이를 통해 prop drilling을 해소하고 데이터 전달을 보다 간편하게 할 수 있습니다. - 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-
'학습 > kakao X goorm 풀스택12회차' 카테고리의 다른 글
[React] React.memo, useCallback, useMemo (0) | 2025.01.08 |
---|---|
[특강] 소프트웨어 개발 방법론에 의한 이슈트래커, 협업도구, 생산성 도구(1/6 1차 강의) (0) | 2025.01.06 |
[JavaScript] 디자인 패턴 정리 (2) | 2024.12.14 |
[JavaScript] Symbol (0) | 2024.12.13 |
[JavaScript] Callbacks, Promise, Async/Await (0) | 2024.12.12 |