리덕스는 무엇인가?
공식 문서에는 "액션"이라는 이벤트를 사용해 어플리케이션의 상태를 관리하고 업데이트하기 위한 패턴 및 라이브러리로 설명되어 있다. 즉, 상태가 예측 가능한 방식으로 데이터를 업데이트할 수 있도록 보장하는 규칙과 전체 어플리케이션에서 사용해야 하는 중앙 저장소 역할을 한다고 한다.
리덕스는 리액트 뿐만 아니라 뷰(Vue)나 앵귤러 등 자바스크립트를 사용하는 프레임워크는 모두 적용할 수 있는 라이브러리로, 리액트에 State Props Drilling을 방지하고 컴포넌트 단에서 쉽게 State에 접근 할 수 있다.
상태 예측이 가능한 방식으로 업데이트가 된다고 소개되어 있는데, 이 말은 저장되는 State의 규칙이나 방식을 사용자가 직접 정의하기 때문이다.
리덕스를 왜 사용해아할까?
하나의 리액트 앱은 상위 App 컴포넌트를 필두로 트리구조의 형태로 이루어져 있다. 각 컴포넌트마다 State 값을 갖는데 이 값을 공유하려면 컴포넌트끼리 경유하며 값을 전달해야 한다. 이것을 Props Drilling이라고 부른다.
그래서 리액트에서는 하나의 값을 전역 상태로 관리하기 복잡하고 힘들다. 처음은 쉽지만 나중에 그것도 다른 사람이 유지보수할 때, 지옥이 펼쳐질 것 🤯
리덕스는 어플리케이션의 전역 상태 관리에 효과적이고 리덕스 이외에 Mobx, Recoil 등이 있다. 하지만 간단한 State 관리만 필요하다면 리액트 자체적으로 제공하는 context API 훅을 사용하는 것도 나쁘지않다.
필요하신 분들은 아래 링크를 참고해주세요.
리덕스는 자체 제공하는 패턴과 도구를 사용하면 어플리케이션의 상태가 언제, 어디서, 왜, 어떻게 업데이트 되는지 이러한 변경이 발생할 때 로직이 어떻게 작동하는지 쉽게 이해할 수 있고 합니다.
크롬 브라우저 확장 프로그램으로 리덕스 툴킷을 설치해 개발자 도구에서 사용하면 디버깅하는데 좋다.
리덕스 사용시 주의사항
1. 하나의 어플리케이션에는 하나의 스토어만 사용한다.
이것은 권장사항이며, 필요에 따라 사용할 수 있다.
2. 상태는 읽기 전용이다.
기존 리액트의 State처럼 상태를 수정하지 않고 새로운 상태로 교체하는 방법으로 업데이트해준다. (불변성을 유지) 데이터의 변경을 감지하기 위해 기존 상태의 객체를 새로운 객체로 변경해주면, 객체 주소가 달라지므로 변경을 쉽게 감지할 수 있다.
3. 리듀서는 순수 함수이다.
리듀서는 이전 상태와 액션 객체를 파라미터로 받는다. 이전의 상태는 건드리지 않고 새로운 객체를 만든다. 동일 인풋에 대한 동일 아웃풋이 보장되어야 한다.
리덕스 설치
# NPM
npm install redux react-redux
# Yarn
yarn add redux react-redux
Provider 컴포넌트로 App 래핑
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { store } from './redux/Store';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom'
import './style/App.css'
ReactDOM.render(
//리덕스 스토어
<Provider store={store}>
<BrowserRouter basename='/'>
<React.StrictMode>
<App />
</React.StrictMode>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
리덕스 폴더 구조
src
ㄴ Redux
ㄴ store.js // store정의
ㄴ Reducer.js // state변경 로직 작성
ㄴ Types.js // action.type 정의
ㄴ Actions.js // type별 로직에 대한 함수 파라미터 정의
리덕스를 사용하는데 여러 패턴이 있습니다. 덕덕스 패턴 등.. 프로젝트 규모가 작다면 굳이 특정 패턴을 따라 사용하지 않아도 문제없고, 이해할 수 있고 편한 방법을 사용하는 것이 중요하다고 생각합니다. 괜히 러닝커브만 높아지면 유지보수나 협업하는데 비효율적이니깐요.
제 경우 위와 같은 폴더 구조로 만들어봤습니다.
리덕스 사용법
1. 스토어 정의
// store.js
import { createStore } from 'redux'
import { reducer } from './Reducer';
export const store = createStore(reducer);
store는 저장 공간으로 생각하고 한 번 생성해두면 사용할 일이 잘 없습니다.
2. 액션(Action) : State를 변경하는 행위
//Actions.js
import { UP_COUNT, DOWN_COUNT } from "./Types"
export const upCount = () => ({ // 카운트를 증가시키는 액션
type : DOWN_COUNT,
})
export const downCount = () => ({ // 카운트를 감소시키는 액션
type : UP_COUNT,
})
// Types.js
// 리덕스 로직에서 사용하며 위 폴더 구조에서
// Actions.js와 Reducer.js 에서 사용
export const UP_COUNT = 'UP_COUNT';
export const DOWN_COUNT = 'DOWN_COUNT';
Action은 리덕스에서 State를 변경하는 행위를 말하며, Types.js에는 State를 변경하는 행위를 구별할 수 있는 문자열을 선언해줍니다. 'UP_COUNT' 와 같이 한 눈에 봐도 알 수 있도록 만들어줍니다.
변경이 필요할 경우 한 번의 변경으로 모두 바뀌어 사용할 수 있도록 Types.js 파일로 분리해줬습니다.
내부 로직에서는 리덕스 액션에 정의한 함수를 사용합니다. 해당 액션을 실행할 때는 dispatch라는 리덕스 내장함수에 넣어 실행시켜줍니다. dispatch() 함수 파라미터에는 객체 형식으로 타입에 액션을 전달해야하는데요.
dispatch({ type : 'UP_COUNT' });
하지만 Types.js에 이미 행동을 구별할 수 있는 문자열을 선언해 주었고 Actions.js 에서 이를 받아와 해당 행위를 하는 함수는 만들어 주었습니다.
그러므로 결론적으로 리덕스를 제외한 내부 로직에서는 Actions.js에 있는 함수들만 내부 로직에 import 시켜 dispatch() 함수에 넣어 사용하면 됩니다.
파라미터를 전달해야하는 경우 Actions.js에 이를 정의해놔야 합니다.
//Actions.js
import { UP_COUNT, DOWN_COUNT } from "./Types"
export const downCount = (num) => ({
type : UP_COUNT,
num : num,
})
export const upCount = (num) => ({
type : DOWN_COUNT,
num : num,
})
4. 리듀서(Reducer)
// Reducer.js
import { UP_COUNT, DOWN_COUNT } from "./Types";
const initalState = 0;
export const reducer = (state = initalState, action) => {
switch(action.type){
case UP_COUNT : {
return state+1;
}
case DOWN_COUNT : {
return state-1;
}
default : return [...state];
}
}
Reducer.js는 dispatch를 통해 받은 action을 구분해서 State를 실질적으로 업데이트하는 로직 입니다. reducer 함수에는 전역관리를 위한 state와 action 매개변수를 받고 디스패치가 실행되면 switch문이나 If문으로 action.type을 구분해서 값을 업데이트해줍니다.
디스패치에 파라미터를 넘겨줬다면 reducer 함수의 action 파라미터에 담겨있으니 사용하면 됩니다.
4. 디스패치(dispatch)
dispatch는 위에서 설명했듯 리덕스 스토어의 내장 함수입니다. dispatch에 액션과 필요한 데이터를 파라미터로 담아 보내면 리덕스 스토어에서 리듀서를 실행시켜 해당 액션에 따라 정의한 대로 State를 변경합니다.
dispatch 사용 방법은 아래와 같습니다.
//내부 로직
import { useDispatch, useSelector } from "react-redux"
import { upCount, downCount } from "../redux/Actions"
const Index = () => {
const dispatch = useDispatch();
const state = useSelector(state => state);
const add_Count = () => { dispatch( upCount() ) }
const remove_Count = () => { dispatch( downCount() ) }
return(
<>
{state}
</>
)
}
export default Index