🐕 Redux
Redux는 상태관리 라이브러리이며 리액트에 종속되는 라이브러리가 아닙니다.
따라서 리액트 환경 뿐만 아니라 다양한 환경 심지어는 바닐라 자바스크립트에서도
리덕스를 사용할 수 있습니다.
리덕스는 하위 컴포넌트의 상태 공유를 위해서 상태 끌어올리기와 props drilling을 반복해야한다는
리액트의 구조에서 기인한 문제점을 해결해줍니다.
컴포넌트의 깊이가 깊지 않은 경우에는 props를 사용해서 상태를 내려주는 것도
상태 끌어올리기를 통해 서로 다른 하위 컴포넌트에서
사용해야 할 상태를 상위 컴포넌트에서 관리하는 것 또한 그리 귀찮지 않은 범위에서 가능하지만
구조의 복잡도가 심해지고 깊이가 깊어질수록 이런식의 데이터흐름은 복잡도가 올라갑니다.
어느정도의 시점까지는 필요한 상태를 최대한 가까운 상위 컴포넌트에 배치하는것으로 해결할 수 있지만
만약 애플리케이션의 복잡도가 심할 경우 구조때문에 코드가 복잡해지게됩니다.
따라서 이를 해결하기 위해 FLUX 디자인 패턴의 도입을 고려해볼 수 있고
이것을 리액트는 리덕스를 통해 해결합니다.
정말 눈물나는 역사가 아닐 수 없는.. 이런 역사가 생기게 된 계기는
리덕스가 가진 장황한 문법에 기인합니다.
리덕스 코어는 가벼운 대신 많은 부분을 개발자가 직접 작성해야했고
이는 리덕스의 악명높은 보일러플레이트 문제를 불러왔습니다.
리덕스의 장황한 문법에 저도 여러번 고생을 겪었는데..
그걸 또 쉽게 만들어주는 redux-action이나 react-redux같은 대안들도 그다지...
편해진다는 느낌은 없었습니다.
오히려 react-redux가 제공하는 connect 함수는 라이브러리의 단일 함수 중
가장 러닝커브가 높다라고 느껴진 함수였습니다.
알고 난 다음엔 별 거 없어보이지만 처음엔 그 생소한 구조가 주는 파괴력이 있다고해야할까요
만약 자바스크립트를 잘 모르고있는 상태였다면 그냥 사용을 포기했을 것 같아요
오히려 제공되는 라이브러리들의 함수가 어떤 일을 하는 애들인지
파악하는 데에 시간이 들어서 이시간에 그냥 쌩으로 작성하는게 나을지도..? 란 생각이 들었답니다.
하지만 리덕스툴킷은 이런 보일러플레이트를 획기적으로 줄여주고
리덕스 자체를 쉽게 만들어준다고 합니다!
아주 기대되니까 공식문서를 따라가보겠습니다.
그이전에 flux패턴에 대해서도 알아보고요!
👻FLUX의 기본적인 구조
https://bestalign.github.io/translation/cartoon-guide-to-flux/
는 아주 좋은 레퍼런스가 있으니 이걸 참고합시다.
중요한 포인트는 단방향 데이터 흐름이며
Action -> Dispatcher -> reducer -> store의 흐름으로 갑니다.
각 요소의 역할은 다음과 같습니다.
Action | 변경될 상태에 대한 정보를 담고 있어야합니다. |
Dispatcher | Action객체를 Reducer함수에 전달하는 역할을 합니다. |
reducer | Reducer 함수는 전달받은 Action객체의 정보를 기반으로 전역 상태 저장소 Store의 상태를 변경하는 역할을 합니다. |
store | store는 전역 상태를 저장하는 창고와 같은 역할을 수행합니다. |
1. 상태가 변경되어야 하는 이벤트가 발생하면 Action 객체를 생성합니다.
2. Action 객체를 Dispatch 함수의 인자로 전달합니다.
3. Dispatch함수는 Action 객체를 Reducer에게 전달해줍니다.
4. Reducer 함수는 전달받은 Action 객체의 값을 토대로 store의 상태를 변경시킵니다.
5. store의 상태가 변경되면 React는 화면을 다시 렌더링합니다.
하나하나의 역할을 살펴보면 확실히 분업화되어 인식하기가 쉽습니다.
🥶 store
store는 앞서 말했듯 상태가 관리되는 오직 하나뿐인 저장소의 역할을 합니다.
즉 상태를 저장하는 공간입니다.
기존에는 createStore라는 함수를 이용해 store를 생성했지만
import { createStore } from 'redux'
const store = createStore(rootReducer)
현재 시점에서는 레거시 취급이 되는 것인지 vs code에서 createStore를 사용하면
취소선이 그어집니다.
동작 자체는 문제없이 되기 때문에 무시하고 사용하셔도 됩니다만
공식문서에나 createStore의 안내에 따르면 이제 createStore 대신
configureStore를 사용할 것을 권합니다.
configureStore는 다음과 같습니다.
https://redux.js.org/tutorials/fundamentals/part-8-modern-redux#using-configurestore
공식문서를 통해 참고해볼 수 있습니다.
configureStore는 리덕스툴킷에 추가된 새로운 함수로
임포트해오는 주소 또한 @reduxjs/toolkit 인걸 확인할 수 있습니다.
관심이 끌리는 지점은 configureStore가 createStore의 설정을 자동으로 처리해준다는 부분인 것 같습니다.
리덕스 툴킷을 이용한 작성방법은 아래에서 다루도록합니다.
😋reducer
전통적인 방식의 리덕스에서는 그냥 리듀서를 만들어주면 됩니다..
function todos(state = initialState, action) {
switch (action.type) {
case CHANGE_INPUT:
return {
...state,
input: action.input,
};
case INSERT:
return {
...state,
todos: state.todos.concat(action.todo),
};
case TOGGLE:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo,
),
};
case REMOVE:
console.log('안녕');
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
}
like this...
if, else if문으로 작성해도 되고
switch 문으로 작성해도 되지만 전 switch 문으로 작성하는 것을 좀 더 선호하는 편입니다.
🥶 Dispatcher
보통 리액트리덕스 라이브러리가 제공하는
useDispatch 혹은 Connect 함수로 불러와서 사용합니다.
🥶 Action
React-actions 라이브러리가 제공하는 createAction 함수를 사용하는 방법과
그냥 내가 액션생성함수를 작성하는 방법
아니면 그때그때 액션객체를 작성해주는 방법 등이 존재합니다.
액션생성함수의 기본적인 형태는 다음과 같습니다
const actionMaker = () => {
return {type:"엄준식"}
}
🌞전통적인 리덕스 방식으로 리덕스를 사용하기
리덕스 코어는 의도적으로 가볍게 만들어 개발자의 자유도를 높인 라이브러리입니다.
하지만 그 덕분에 간단한 기본 기능을 빠르게 구현하고 싶을 때에도
개발자는 수많은 보일러플레이트 문법과 싸우며 장황한 코드를 작성해야만 했습니다.
리덕스 툴킷은 이러한 리덕스의 문제점을 해결해주며
간단하게 리덕스에 접근할 수 있도록 도와줍니다.
하지만 그럼에도 불구하고 리덕스 툴킷이 왜 멋있는지 알려면
리덕스가 어떤 형식으로 작성되는지를 알 필요성이 있습니다.
createStore | 말그대로 스토어를 만드는 함수입니다. 매개변수로 보통 리듀서함수를 넣어줍니다. |
Provider | 자식 요소들에게 리덕스의 상태를 사용할 수 있게 해줍니다. 속성으로 store 속성을 가집니다. |
combineReducer | 여러가지 상태를 관리하기 위해서는 여러개의 리듀서 함수를 작성해야합니다. combineReducer는 여러개로 작성된 리듀서 함수들을 하나로 합쳐주는 일을 수행합니다. |
리듀서함수 | 개발자가 직접 작성합니다. 초기값 initialstate와 액션의 타입에 따라 처리를 다르게할 리듀서 함수를 구성하도록 합니다. |
initialstate | 리듀서함수가 가질 초기값을 만들어줍니다. 말이 거창할뿐 가장 쉽습니다. |
액션생성함수 | 필수는 아니지만 액션을 생성하는 함수를 작성하는 경우도 있습니다. 매번 액션객체를 하드코딩해서 넣어주는 방법도 상관...은 없습니다. |
useDispatch | 디스패치함수를 사용하기 위한 리액트리덕스가 제공하는 함수입니다. |
useSelector | store에서 내가 원하는 상태를 가져오기위한 리액트리덕스가 제공하는 함수입니다. |
connect | useDispatch와 useSelector의 역할을 동시에 수행하는 함수입니다. 성능최적화도 자동으로 해준다는 장점이 있지만 구조가 살짝 어려운 탓에 러닝커브가 조금 있습니다. |
네... 어지럽습니다.
저 각각의 함수들의 사용법과 콜백의 매개변수에 어떤 값이 들어오는지까지 공부하다보면
내가 무슨 부귀영화를 누리려고 이걸 하고있나하는 생각이 듭니다.
리덕스 툴킷은 이러한 작업을 획기적으로 줄여주며 문법적인 편의성을 제공합니다.
예컨대 리덕스는 불변성을 지켜야한다는 제약조건이 있어
스프레드 연산자등의 방법을 통해 참조자료형의 경우 복사를 하여 값을 수정해야하지만.
리덕스 툴킷은 이러한 작업을 마치 마법처럼 처리해줍니다.
😋리덕스 툴킷에서는 어떻게 처리할까?
리덕스 툴킷에서 위에 작성해둔 작업들을 처리하는 것은 획기적으로 쉬워집니다.
다음은 리덕스툴킷을 이용해 위 함수들을 대체하고자 할 때 사용할 수 있는 함수 집합입니다.
configureStore | createStore를 대체합니다. 그런데 combinereducer의 역할도 같이하는.. |
connect | 얘는 그냥 쓰시면 됩니다. |
useDispatch | 얘도 그냥 쓰시면 됩니다. |
useSelector | 얘도 그냥 쓰시면 됩니다. |
Provider | 얘도 그냥 쓰시면 됩니다. |
createSlice | initialState와 리듀서함수 작성과 기존 action 생성함수가 하던 일을 이 함수 내부에서 간단한 문법으로 모두 처리할 수 있습니다. |
복잡한 보일러플레이트가 확 줄어들고
이정도면 배워볼만한데..? 라는 생각이 들게 배워야할 것들이 적어진걸 알 수 있습니다.
이제 redux toolkit 으로 들어가봅시다.
🤢redux-toolkit을 시작해보자
https://redux-toolkit.js.org/introduction/getting-started
npm install @reduxjs/toolkit
이렇게 리덕스 툴킷을 설치해줍니다.
npm install react-redux
우린 react-redux가 제공하는 useDispatch , useSelector, connect 함수를 활용할 것이기 때문에
react-redux도 같이 깔아주겠습니다.
리덕스를 공부하실 정도면 react 개발환경은 이미 알아서 잘 설정해두셨을테니 스킵합니다.
🤮createSlice
우선 관리할 상태의 초깃값과 리듀서 함수의 역할을 수행할 코드를 작성할 것입니다.
열심히 initialstate 변수 만들어주고 switch 문을 작성해도 되지만
좀 더 간단하게 리덕스 툴킷이 제공하는 createSlice 함수를 이용하여 작성해봅시다.
간단한 counter를 하나 만들어볼 생각입니다.
src 폴더 내부에 modules라는 폴더를 만들어 리듀서 로직들을 관리하겠습니다.
src/modules/counterSlice.jsx
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counterSlice',
initialState: { value: 0 },
reducers: {
up: (state, action) => {
state.value = state.value + action.payload;
},
},
});
export default counterSlice;
export const { up } = counterSlice.actions;
먼저 createSlice를 import 해온 뒤
createSlice의 파라미터에 객체를 전달합니다.
객체의 키값에 유의하며 작성해야합니다.
createSlice()는 객체를 반환하는데 이때 반환하는 객체의 키 밸류에 들어갈 값들이
지금 작성하는 객체의 키값과 연관성이 있기 때문입니다.
다음은 우리가 작성한 createSlice 함수의 반환값을 콘솔에 찍어본 결과입니다.
우리가 reducers키에 작성한 값이 actions 키값에 들어가게 된 것을 확인할 수 있습니다.
reducer로 작성하시면 안됩니다...
reducers로 작성하셔야해요
1. name : 중복을 피하기 위해 사용하는 고유한 값입니다.
2. initialState : default 값이면서 동시에 상태 관리에 사용하는 속성입니다.
3. reducers : 상태 변화를 처리하는 함수를 정의합니다.
기존 리덕스에서는 액션 타입을 지정하고 타입에 따른 액션 생성함수를 만들고
action.type에 따른 상태 변화를 처리하고.. 불변성을 지키면서 상태를 변화시켰다면
리덕스 툴킷에서는 하나의 함수를 정의하는 것으로 모든 작업을 끝냅니다.
심지어 불변성도 알아서 마법처럼 처리해주기 때문에 스프레드 문법 또한 사용하지 않아도 좋습니다.
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counterSlice',
initialState: { value: 0 },
reducers: {
up: (state, action) => {
state.value = state.value + action.payload;
},
},
});
export default counterSlice;
export const { up } = counterSlice.actions;
다시 동일한 코드를 보겠습니다 위 코드와 같으니 안 올려보셔도 됩니다.
reducers 키에 객체를 할당하는데 이름을 설정하고
initialState를 설정해줍니다. initialState는 전 객체로 주지만
객체일 필요가 없는 경우에는 객체로 주지 않아도 상관없습니다.
reducers 에는 객체형태로 함수를 작성해서 넘겨줍니다.
함수는 매개변수로 state와 action을 받을 수 있습니다.
state는 우리가 initialstate에 넣은 값 혹은 현재 상태에 접근할 수 있게 합니다.
따라서 우리의 initialState는 객체형태로 작성했고 안에 value라는 키를 넣어뒀으니
속성에 접근하기위해서 state.value로 접근합니다.
반면 action은 우리가 dispatch로 전달한 액션객체가 전달됩니다.
모두 작성했다면 counterSlice와 우리가 만든 up메서드를 추출해서 내보내기합니다.
앞서 보았듯이 createSlice의 반환값에는 actions에 우리가 작성한 up 메서드가 들어있으니까
counterSlice.actions로 접근하여 구조분해할당하여 내보내기해줍니다.
🤮configureStore
여러개의 리듀서를 합치는 상황을 설정하고자
하나의 리듀서를 더 작성했습니다. 내용은 아무래도 상관없으니
그냥 N개의 리듀서를 합치는 작업을 한다고 생각하겠습니다.
src/modules/index.jsx
import counterSlice from './counterSlice';
import { configureStore } from '@reduxjs/toolkit';
import junsickReducer from './junsickReducer';
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
umjunsick: junsickReducer.reducer,
},
});
export default store;
configureStore를 import 해온 뒤 configureStore의 파라미터에
객체를 넣어줍니다.
키값은 reducer로 설정합니다(reducers가 아닙니다)
그 뒤 reducer의 밸류에 객체를 하나 작성하고
이름을 지어줍니다. counter라고 이름을 지어주도록하겠습니다.
counter : counterSlice.reducer 와 같이 반환값의 리듀서를 값으로 전달합니다.
그런 다음 store를 내보내기 해주겠습니다.
src/components/Counter.jsx
상태를 받아와서 사용할 컴포넌트를 만들어주겠습니다.
우선은 스토어에 우리가 만든 상태를 연결해주는 것부터 할것이니
파일을 생성만 하고 넘어가겠습니다.
src/App.jsx
import { useState } from 'react';
import './App.css';
import Counter from './components/Counter';
import { Provider } from 'react-redux';
import store from './modules';
const App = () => {
return (
<Provider store={store}>
<div>
<Counter />
</div>
</Provider>
);
};
export default App;
Provider와 우리가 곧 작성할 Counter 컴포넌트 위에서 내보낸 store를 받아옵니다.
이때 Provider는 react의 Provider가 아니라 react-redux의 Provider를 import 해야 합니다.
최상위를 provider로 감싸주고 store 속성에 우리가 작성한 store를 연결해주겠습니다.
이제 Counter 컴포넌트에서 리덕스를 사용할 준비가 완료되었습니다.
🤮useSelector와 useDispatch를 사용해서 연결하자
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { up } from '../modules/counterSlice';
const Counter = () => {
const dispatch = useDispatch();
const count = useSelector(state => {
return state.counter.value;
});
const onClick = () => {
dispatch(up(2));
};
return (
<div>
<h2>{count}</h2>
<h2>하이요</h2>
<button onClick={onClick}>+</button>
</div>
);
};
export default Counter;
useSelector와 useDispatch를 사용하여 상태를 연결하는 코드입니다.
우리가 내보낸 up 함수를 import 해오고 useSelector와 useDispatch도 import 합니다.
useDispatch의 반환값을 변수에 담습니다
useDispatch의 반환값이 dispatcher의 역할을 수행해줄 것입니다.
useSelector는 우리가 접근하고싶은 상태에 접근하게 해줍니다.
그런 뒤 버튼이 클릭되면 카운터의 상태를 변경할 수 있도록 코드를 작성할 것입니다.
dispatch에 액션을 넘겨주는 방법은 크게 두가지를 들 수 있습니다.
const onClick = () => {
dispatch({type:'counterSlice/up , step:2});
};
const onClick = () => {
dispatch(up(2));
};
위 두가지 코드는 비슷하게 동작합니다.
다만 위와 같이 step값을 지정하여 작성하는 경우에는
up함수의 코드를 action.step 값으로 변경해주어야 할 것입니다.
반면 액션객체가 아니라 우리가 만든 up함수를 그대로 가져와서
안에 우리가 넣어주고싶은 값을 넣은 경우
이 값은 up 함수에게 전달되며 우리가 파라미터에 넣은 값 2는
payload 에 담겨 전달됩니다.
어떻게 작성하든 상관은 없으니까 편한대로 작성하시면 되겠습니다.
만약 상태를 객체로 관리하고 싶다면 객체로 만든다음 그 객체의 키값들을 변경해주면 됩니다.
다음은 connect를 활용하여 작성하는 예시입니다.
connect 도 살짝의 러닝커브만 극복하면 아주 유용하고 간결하게 작성할 수 있습니다.
위 그림의 코드는 신경쓰지 않으셔도 좋습니다.
커넥트 함수는 함수를 반환하는 고차함수이다.와 같은 텍스트만 집중해서 읽어주시면 되겠습니다.
다만 저 그림의 코드는 redux-toolkit을 적용하지 않은 코드이기 때문에
redux-toolkit을 적용한 예제를 작성해보겠습니다.
import React from 'react';
import { connect } from 'react-redux';
import { up } from '../modules/counterSlice';
const Counter = ({ count, onClick }) => {
return (
<div>
<h2>{count}</h2>
<h2>하이요</h2>
<button onClick={onClick}>+</button>
</div>
);
};
const mapStateToProps = state => {
return {
count: state.counter.value
};
};
const mapDispatchToProps = dispatch => {
return {
onClick: () => {
dispatch(up(2));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
이렇게 코드를 작성할 수 있습니다.
하지만 뭔가..장황하죠..? 그걸 위해서 리액트리덕스는 편의성을 개선해뒀습니다.
우리가 작성한 저 장황한 문법은 이런 형태로 간단하게 표현할 수 있어졌습니다.
두번째 인자에 객체형태의 값을 넣어주면 connect 함수가 내부적으로
bindActionCreators 작업을 대신 해주기 때문에
위와 같이 작성해도 멋지게 동작하는 화면을 볼 수 있습니다.
import { connect } from 'react-redux';
import { up } from '../modules/counterSlice';
connect와 우리가 사용해주고싶은 reducer함수를 import하고..
const Counter = ({ count, up }) => {
return (
<div>
<h2>{count}</h2>
<h2>하이요</h2>
<button onClick={() => up(2)}>+</button>
</div>
);
};
export default connect(state => ({ count: state.counter.value }), { up })(Counter);
props에 객체구조분해할당문법으로 값을 받아와줍니다.
어떻게 리덕스의 store와 컴포넌트의 props가 연결되는지는
connect 함수에서 결정되게 됩니다.
위와 같이 코드를 작성하면 끝입니다!
dispatch로 감싸주는 작업 역시 connect 내부에서 이루어지기때문에
useSelector , useDispatch를 사용할때보다 더욱 간결해진 상태를 확인할 수 있습니다.
🐶마치며
FLUX 패턴 자체는 이해하는게 많이 어렵진 않지만 리덕스의 장황한 문법과 그것을 도와주는 마법같은 일들이 많이 겹쳐있어서 처음 사용법을 익히는게 어려운 것 같습니다.
어떤 구조로 코드가 동작하는지 이해가 안되면 응용하는 것을 힘들어하는 저같은 분들은 더욱 익히기 힘들었을 것 같아요 아주 간단한 사용법을 익혀봤으니 열심히 응용해보고 미들웨어를 다루는 것 까지 해보면 좋겠습니다.
참고한 레퍼런스는 다음과 같습니다
https://www.robinwieruch.de/react-redux-tutorial/
https://redux.js.org/introduction/why-rtk-is-redux-today
https://redux-toolkit.js.org/api/configureStore
'redux' 카테고리의 다른 글
RTK Query 데이터에 local Storage를 사용하는 방법 (0) | 2023.05.15 |
---|---|
RTK Query의 Middleware에 대하여 (0) | 2023.05.14 |
updateQueryData RTKquery로 클라이언트에서만 쿼리 데이터를 조작하고 싶다면 (1) | 2023.05.14 |
예제와 함께 TS ReduxToolkit Query Slow start (1) | 2023.05.01 |
상태관리 라이브러리 리덕스 이해하기 with parcel.. (0) | 2023.04.03 |