4 minute read

 Redux는 자바스크립트의 상태 관리 라이브러리 중 하나로, 또 다른 라이브러리로는 Recoil과 Zustand가 있지만 여전히 가장 많이 쓰이면서도 대규모 시스템에 가장 적합한 것은 Redux이다.

 리액트 프로젝트에서 상태 관리를 해야 할 때 주로 Redux를 사용하는데, 그 이유는 보통 리액트에서 사용하는 state는 부모 컴포넌트에서 자식 컴포넌트로 넘어갈 때 props를 이용해 전송되게 된다. 그러나 이 방식을 사용하게 되면 props를 과도하게 많이 사용하게 되거나, state를 사용할 필요가 없는 중간 컴포넌트까지도 단지 state를 전송하기 위한 목적으로 props를 받아오게 되는 일이 생겨날 수 있다. 이를 props drilling 문제라고 한다.

 Redux는 전역으로 state를 공유할 수 있는 상태 저장소인 store를 두고, 여기에 저장된 state에는 개발자가 정의한 reducer를 통해 값을 변경할 수 있도록 만드는 방식으로 state를 관리한다. 이를 통해서 기존의 props drilling 문제를 해결할 수 있으며, 상태의 값과 그 변화를 한 곳에서 관리하기 때문에 상태를 관리하기가 편해지게 된다.

 Redux toolkit은 기존의 Redux가 너무 과도한 보일러플레이트를 가지는 단점을 해결하기 위하여 Redux를 간단하게 쓸 수 있도록 만들어진 개선된 버전이다. Redux 개발팀에서는 이를 이용해 Redux를 사용할 것을 권장하고 있다.


설치

 먼저 터미널에 입력해야 하는 것이 있다.

npm install @reduxjs/toolkit react-redux

초기 세팅

main.js
import App from './App.jsx'
import { Provider } from 'react-redux'
import App from './App.jsx'
import store from './store.js'	// 따로 만들어야 한다.

createRoot(document.getElementById('root')).render(
	<Provider store={store}>
		<App />
	</Provider>
)

 main.jsx 파일을 위와 같이 만들어줘야 한다. Provider 컴포넌트가 App 컴포넌트를 감싸는 구조로 만들면 된다.

 위의 코드에서 store는 리덕스의 상태 저장소로, 따로 store.js 파일을 만든 후 import해 온다.

store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from '../slices/counterSlice.js';

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

export default store;

 store.js 파일은 위와 같은 구조로 만들어진다. 여기에서 configureStore()는 리덕스 툴킷에서 제공하는 store 생성 함수이다. 이 함수는 필수 파라미터인 reducer로 { key: reducer } 객체를 받고, 이것이 전역 state의 트리 구조를 결정한다.

 위의 코드에서는 reducer의 예시로 { counter: counterReducer }가 들어가 있는데, 여기서 counter가 키, counterReducer는 slice에서 가져온 reducer 함수이다. 각각의 slice 파일은 별도로 정의되어 있지만, store는 reducer 함수만이 필요하기 때문에 각각의 slice 파일에서 reducer 함수를 import 해 온다.

 configureStore()는 store를 만들어서 반환하고, 이를 export하면 된다.

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
    name: 'counter',
    initialState: { value: 0 },
    reducers: {
        increment: (state) => { state.value += 1 },
        decrement: (state) => { state.value -= 1 },
    },
});

export default counterSlice.reducer;
export const { increment, decrement } = counterSlice.actions;

 createSlice()는 리덕스 툴킷에서 제공하는 slice 생성 함수이다. 특정 state와 그것을 수정하는 reducer + action을 한 번에 정의한다.

 이 함수의 파라미터로는 name, initialState, reducers가 있다.

name은 slice의 이름을 정하고, 이 이름은 action type에 prefix로 사용된다.

initialState는 이 slice가 관리할 state의 초기값을 설정한다.

reducers는 state 변경을 정의하는 함수의 모음이다.


리덕스의 핵심

 리덕스를 이해하기 위해서는 state, action, reducer 이 세 가지가 어떻게 동작하는지를 이해해야 한다.

state는 상태 데이터로, store 내부에 저장된다.

action은 어떠한 일이 발생했을 때 만들어지는 객체로, reducer에 전달된다.

reducer는 (현재 state, action) 정보를 넘겨받아 새로운 state를 계산한다.

 과거 리덕스는 action type 정의, action creater 작성, reducer 작성을 각각 따로 해야 했고, 이건 길고 반복적이라는 단점이 있었다. 이 문제를 해결하기 위해 리덕스 툴킷에서 생겨나 도입된 개념이 slice이다.

 slice는 state, action, reducer를 한 파일에서 정의하여 관리한다. 이를 통해 코드 작성 방식을 단순화시키고 유지보수를 간편하게 한다. 그리고 store에서는 reducer 함수만이 필요하기 때문에, 각각의 slice에서는 reducer 함수만을 export 해두면 된다.


state 값 가져오기

리덕스에서 state의 값을 가져와 사용하기 위해서는 useSelector를 사용하면 된다.

import { useSelector } from 'react-redux'

const Screen = () => {
    const count = useSelector((state) => state.counter.value);

    return (
        <>
            <h1>{count}</h1>
        </>
    )
}

export default Screen;

 useSelector는 store의 state를 읽어오는 리액트 훅이다. 위 코드의 useSelector문에서 매개변수인 state는 store의 전체 state 객체이다. 여기서 state.counter.value는 그 안의 counter라는 slice의 value 속성만 꺼내는 것이다. 이를 count 변수에 저장함으로써 값을 사용할 수 있게 된다.


state 값 업데이트

리덕스에서 state의 값을 업데이트하기 위해서는 dispatch를 사용한다.

import { useDispatch } from 'react-redux';
import { increment, decrement } from '../slices/counterSlice';

const Buttons = () => {
    const dispatch = useDispatch();

    return (
        <div>
            <button onClick={() => dispatch(decrement())}>-1</button>
            <button onClick={() => dispatch(increment())}>+1</button>
        </div>
    )
}

export default Buttons;

 리덕스에서 state의 값을 업데이트하는 것은 action을 dispatch하는 것으로 이루어진다. 즉, action 객체를 store로 보내서, reducer가 state를 바꾸도록 지시하는 것이다.

 원래 리액트에서는 useState()로 state를 만들면, 그 값을 변경할 수 있는 set 함수로 컴포넌트에서 직접 state의 값을 변경할 수 있었다. 그러나 리덕스에서는 각 컴포넌트에서 store의 reducer에 값 변경을 요청하게 하고, 직접 값을 수정하는 것을 허용하지 않는다. 이런 방식을 사용하는 것은 state가 전역이기 때문에 어느 컴포넌트에서든 값을 수정할 수 있게 된다면 버그가 생겼을 때 어떤 컴포넌트에서 발생했는지 추적하기 어렵기 때문에 이러한 문제를 해결하려고 한 것이다.

 따라서 state를 바꾸고 싶을 때는 dispatch(action) 을 통해 값 변경을 요청한다. 여기에서 dispatch는 아래와 같이 정의한다.

const dispatch = useDispatch();

 useDispatch() 는 store의 dispatch() 함수를 리턴해준다. 이렇게 쓰는 이유는 매번 useDispatch()를 적는 것보다 dispatch로 사용하는 것이 훨씬 가독성이 좋고 간편하기 때문이다. 이는 단지 코딩 컨벤션으로 필수는 아니다.

 dispatch는 action을 store에 넘겨주는 역할을 한다. 여기에서 action는 ‘무슨 일이 일어났는지를 설명하는 자바스크립트 객체’이다. action은 type과 payload 값을 가지는데, type은 어떤 reducer 함수를 실행할 것인지에 대한 데이터를, payload는 reducer 함수 실행에 필요한 추가 데이터를 담고 있다.

 다만, 리덕스 툴킷에서는 action이 사실상 추상화되어 있기 때문에 개발자가 직접 다룰 일은 없다.


전체적인 흐름

① 터미널로 @reduxjs/toolkitreact-redux 라이브러리를 설치한다.

② main.js에서 <Provider store={store}><App />을 감싼다. 이때 Providerreact-redux에서 import 해 오고 store는 별도로 만든 store.js 파일에서 import 해 온다.

③ store.js를 만든다. configureStore@reduxjs/toolkit으로부터 import 하고, 사용할 Reducer도 slice 파일로부터 import 해 온다.

const store = configureStore({})를 통해 reducer를 만들고, 이를 export default store를 통해 내보낸다.

④ 이번에는 store에서 사용할 slice 파일을 만든다. 먼저 createSlice@reduxjs/toolkit으로부터 import 해 온다.

그 다음으로는 const slice = createSlice({})로 slice를 생성한다. 내부에는 name, initialState, reducers를 각각 정의한다.

마지막으로 이를 export default slice.reducer;export const { (reducer 함수들) } = slice.actions;로 내보낸다.

⑤ 값을 가져올 때는 useSelector를 사용한다. store에 저장된 값을 사용할 컴포넌트에서 useSelectorreact-redux로부터 import 해 온다.

그럼 useSelector((state) => {state.slice.value})로 값을 가져와 변수에 저장할 수 있다.

⑥ 값을 업데이트하고 싶을 때는 useDispatch를 사용한다. 값을 업데이트하는 로직이 있는 컴포넌트에서 useDispatchreact-redux로부터 import 해 온다. 또한 필요한 reducer 함수들도 slice 파일로부터 import 해 온다.

컴포넌트 내부에서 const dispatch = useDispatch();로 dispatch를 만든다.

함수를 사용할 때는 dispatch(reducer 함수)로 store의 값을 업데이트할 수 있다.

Updated: