리덕스는 여러 컴포넌트가 동일한 상태를 보고 있을 때 굉장히 유용합니다!
또, 데이터를 관리하는 로직을 컴포넌트에서 빼면, 컴포넌트는 정말 뷰만 관리할 수 있잖아요!
코드가 깔끔해질테니, 유지보수에도 아주 좋겠죠. 🙂
[상태관리 흐름도]
딱 4가지만 알면 됩니다! Store, Action, Reducer, 그리고 Component!
아주 큰 흐름만 잘 파악해도 굳굳!
리덕스는 아주 흔히 사용하는 상태관리 라이브러리입니다.
전역 상태관리를 편히 할 수 있게 해주는 고마운 친구죠!
리덕스 패키지 설치하기
yarn add redux react-redux
리덕스는 데이터를 한 군데 몰아넣고, 여기저기에서 꺼내볼 수 있게 해주는 친구입니다.
아래 용어들은 리덕스의 기본 용어인데, 여러분이 키워드 삼기 좋은 용어들이에요. 앞으로 자주 볼 단어들이니 미리 친해집시다!
(1) State
리덕스에서는 저장하고 있는 상태값("데이터"라고 생각하셔도 돼요!)를 state라고 불러요.
딕셔너리 형태({[key]: value})형태로 보관합니다.
(2) Action
상태에 변화가 필요할 때(=가지고 있는 데이터를 변경할 때) 발생하는 것입니다.
// 액션은 객체예요. 이런 식으로 쓰여요. type은 이름같은 거예요! 저희가 정하는 임의의 문자열을 넣습니다.
{type: 'CHANGE_STATE', data: {...}}
(3) ActionCreator
액션 생성 함수라고도 부릅니다. 액션을 만들기 위해 사용합니다.
//이름 그대로 함수예요!
const changeState = (new_data) => {
// 액션을 리턴합니다! (액션 생성 함수니까요. 제가 너무 당연한 이야기를 했나요? :))
return {
type: 'CHANGE_STATE',
data: new_data
}
}
(4) Reducer
리덕스에 저장된 상태(=데이터)를 변경하는 함수입니다.
우리가 액션 생성 함수를 부르고 → 액션을 만들면 → 리듀서가 현재 상태(=데이터)와 액션 객체를 받아서 → 새로운 데이터를 만들고 → 리턴해줍니다.
// 기본 상태값을 임의로 정해줬어요.
const initialState = {
name: 'mean0'
}
function reducer(state = initialState, action) {
switch(action.type){
// action의 타입마다 케이스문을 걸어주면,
// 액션에 따라서 새로운 값을 돌려줍니다!
case CHANGE_STATE:
return {name: 'mean1'};
default:
return false;
}
}
(5) Store
우리 프로젝트에 리덕스를 적용하기 위해 만드는 거예요!
스토어에는 리듀서, 현재 애플리케이션 상태, 리덕스에서 값을 가져오고 액션을 호출하기 위한 몇 가지 내장 함수가 포함되어 있습니다.
생김새는 딕셔너리 혹은 json처럼 생겼어요.
내장함수를 어디서 보냐구요? → 공식문서에서요! 😉
(6) dispatch
디스패치는 우리가 앞으로 정말 많이 쓸 스토어의 내장 함수예요!
액션을 발생 시키는 역할을 합니다.
// 실제로는 이것보다 코드가 길지만,
// 간단히 표현하자면 이런 식으로 우리가 발생시키고자 하는 액션을 파라미터로 넘겨서 사용합니다.
dispatch(action);
몰라도 되는, 하지만 알면 재미있는 이야기
리덕스는 사실, 리액트와 별도로 사용할 수 있는 친구입니다. 상태관리를 위해 다른 프론트엔드 프레임워크/라이브러리와 함께 쓸 수 있어요.
(1) store는 1개만 쓴다!
리덕스는 단일 스토어 규칙을 따릅니다. 한 프로젝트에 스토어는 하나만 씁니다.
(2) store의 state(데이터)는 오직 action으로만 변경할 수 있다!
리액트에서도 state는 setState()나, useState() 훅을 써서만 변경 가능했죠!
데이터가 마구잡이로 변하지 않도록 불변성을 유지해주기 위함입니다.
불변성 뭐냐구요? 간단해요! 허락없이 데이터가 바뀌면 안된단 소리입니다!
조금 더 그럴 듯하게 말하면, 리덕스에 저장된 데이터 = 상태 = state는 읽기 전용입니다.
그런데... 액션으로 변경을 일으킨다면서요? 리듀서에서 변한다고 했잖아요?
→ 네, 그것도 맞아요. 조금 더 정확히 해볼까요!
가지고 있던 값을 수정하지 않고, 새로운 값을 만들어서 상태를 갈아끼웁니다!
즉, A에 +1을 할 때,
A = A+1이 되는 게 아니고, A' = A+1이라고 새로운 값을 만들고 A를 A'로 바꾸죠.
(3) 어떤 요청이 와도 리듀서는 같은 동작을 해야한다!
리듀서는 순수한 함수여야 한다는 말입니다.
순수한 함수라는 건,
[외울 필요 없어요!]
덕스 구조를 잘 설명해 주는 사이트가 있습니다. 😉 헷갈리실 때 들어가서 읽어보시면 되고, 모듈을 새로 만들 때 복사해서 쓰셔도 좋습니다.
[사이트 바로가기→](https://github.com/erikras/ducks-modular-redux)
```jsx
// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action Creators
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
}
```
일단 폴더부터 만들어요!
src 폴더 아래에 redux라는 폴더를 만들고, 그 안에 modules라는 폴더를 만들어주세요.
modules 아래에 bucket.js라는 파일을 만들고 리덕스 모듈 예제를 붙여넣어주세요.
아래 과정을 하나하나 밟으며 버킷리스트 항목을 리덕스에서 관리하도록 고쳐봅시다!
(1) Action
우리는 지금 버킷리스트를 가져오는 것, 생성하는 것 2가지 변화가 있죠?
두 가지 액션을 만듭시다.
// 액션 타입을 정해줍니다.
const LOAD = "bucket/LOAD";
const CREATE = "bucket/CREATE";
(2) initialState
초기 상태값을 만들어줄거예요! 그러니까, 기본 값이죠.
// 초기 상태값을 만들어줍니다.
const initialState = {
list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
};
(3) Action Creactor
액션 생성 함수를 작성합니다.
// 액션 생성 함수예요.
// 액션을 만들어줄 함수죠!
export const loadBucket = (bucket) => {
return { type: LOAD, bucket };
};
export const createBucket = (bucket) => {
return { type: CREATE, bucket };
};
(4) Reducer
리듀서를 작성합니다.
load할 땐, 가지고 있던 기본값을 그대로 뿌려주면 되겠죠?
create할 땐, 새로 받아온 값을 가지고 있던 값에 더해서 리턴해주면 될거예요!
(우리는 action으로 넘어오는 bucket이 text값인 걸 알고 있죠! 이미 추가해봤잖아요.)
// 리듀서예요.
// 실질적으로 store에 들어가 있는 데이터를 변경하는 곳이죠!
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
return state;
case "bucket/CREATE":
const new_bucket_list = [...state.list, action.bucket];
return { list: new_bucket_list };
default:
return state;
}
}
(5) Store
redux 폴더 하위에 configStore.js 파일을 만들고 스토어를 만들어볼게요!
//configStore.js
import { createStore, combineReducers } from "redux";
import bucket from "./modules/bucket";
// root 리듀서를 만들어줍니다.
// 나중에 리듀서를 여러개 만들게 되면 여기에 하나씩 추가해주는 거예요!
const rootReducer = combineReducers({ bucket });
// 스토어를 만듭니다.
const store = createStore(rootReducer);
export default store;
store를 다 만들었으니 이젠 컴포넌트와 연결할 차례! (끝이 다와가요!)
index.js에서 필요한 작업을 해줄거예요.
스토어를 불러오고 → 우리 버킷리스트에 주입하면 끝!
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from "react-router-dom";
// 우리의 버킷리스트에 리덕스를 주입해줄 프로바이더를 불러옵니다!
import { Provider } from "react-redux";
// 연결할 스토어도 가지고 와요.
import store from "./redux/configStore";
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
// 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
reportWebVitals();
App 컴포넌트에 있는 state를 리덕스로 교체해볼까요?
(1) 컴포넌트에서 리덕스 데이터 사용하기
-1) 리덕스 훅
리덕스도 훅이 있어요!
상태, 즉, 데이터를 가져오는 것 하나, 상태를 업데이트할 수 있는 것 하나 🙂
이렇게 두 가지를 정말 많이 쓴답니다!
더 많은 훅이 궁금하다면? (훅 보러가기→)
// useDispatch는 데이터를 업데이트할 때,
// useSelector는 데이터를 가져올 때 씁니다.
import {useDispatch, useSelector} from "react-redux";
-2) BucketList.js에서 redux 데이터 가져오기
useSelector((**state**) ⇒ state.bucket)
configStore.js에서 루트 리듀서를 만들었던 거 기억하시나요?
앗, 바로 감이 오셨나요? 네, 맞아요! 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.
...
// redux 훅 중, useSelector를 가져옵니다.
import { useSelector } from "react-redux";
const BucketList = (props) => {
let history = useHistory();
// 이 부분은 주석처리!
// console.log(props);
// const my_lists = props.list;
// 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.
// 우리는 그 중, bucket 안에 들어있는 list를 가져옵니다.
const my_lists = useSelector((state) => state.bucket.list);
return (
<ListStyle>
{my_lists.map((list, index) => {
return (
<ItemStyle
className="list_item"
key={index}
onClick={() => {
history.push("/detail");
}}
>
{list}
</ItemStyle>
);
})}
</ListStyle>
);
};
...
-3) App.js에서 redux 데이터 추가하기
useSelector((**state**) ⇒ state.bucket)
configStore.js에서 루트 리듀서를 만들었던 거 기억하시나요?
앗, 바로 감이 오셨나요? 네, 맞아요! 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.
import 부터!
// useDispatch를 가져와요!
import {useDispatch} from "react-redux";
// 액션생성함수도 가져오고요!
import { createBucket } from "./redux/modules/bucket";
useDispatch 훅 쓰기
const dispatch = useDispatch();
const addBucketList = () => {
// 스프레드 문법! 기억하고 계신가요? :)
// 원본 배열 list에 새로운 요소를 추가해주었습니다.
// 여긴 이제 주석처리!
// setList([...list, text.current.value]);
dispatch(createBucket(text.current.value));
};
[코드스니펫] - App.js
import React from "react";
import styled from "styled-components";
import { Route, Switch } from "react-router-dom";
// useDispatch를 가져와요!
import {useDispatch} from "react-redux";
// 액션생성함수도 가져오고요!
import { createBucket } from "./redux/modules/bucket";
// BucketList 컴포넌트를 import 해옵니다.
// import [컴포넌트 명] from [컴포넌트가 있는 파일경로];
import BucketList from "./BucketList";
import Detail from "./Detail";
import NotFound from "./NotFound";
function App() {
const text = React.useRef(null);
// useHistory 사용하는 것과 비슷하죠? :)
const dispatch = useDispatch();
const addBucketList = () => {
// 스프레드 문법! 기억하고 계신가요? :)
// 원본 배열 list에 새로운 요소를 추가해주었습니다.
// 여긴 이제 주석처리!
// setList([...list, text.current.value]);
dispatch(createBucket(text.current.value));
};
return (
<div className="App">
<Container>
<Title>내 버킷리스트</Title>
<Line />
{/* 컴포넌트를 넣어줍니다. */}
{/* <컴포넌트 명 [props 명]={넘겨줄 것(리스트, 문자열, 숫자, ...)}/> */}
<Switch>
{/* <Route
path="/"
exact
render={(props) => <BucketList list={list} />}
/> */}
{/* 이제는 render를 사용해서 list를 넘겨줄 필요가 없죠! 버킷리스트가 리덕스에서 데이터를 알아서 가져갈거니까요! */}
<Route exact path="/" component={BucketList} />
<Route exact path="/detail" component={Detail} />
<Route component={NotFound} />
</Switch>
</Container>
{/* 인풋박스와 추가하기 버튼을 넣어줬어요. */}
<Input>
<input type="text" ref={text} />
<button onClick={addBucketList}>추가하기</button>
</Input>
</div>
);
}
const Input = styled.div`
max-width: 350px;
min-height: 10vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;
const Container = styled.div`
max-width: 350px;
min-height: 60vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;
const Title = styled.h1`
color: slateblue;
text-align: center;
`;
const Line = styled.hr`
margin: 16px 0px;
border: 1px dotted #ddd;
`;
export default App;
[코드스니펫] - BucketList.js
// 리액트 패키지를 불러옵니다.
import React from "react";
import styled from "styled-components";
import { useHistory } from "react-router-dom";
// redux 훅 중, useSelector를 가져옵니다.
import { useSelector } from "react-redux";
const BucketList = (props) => {
let history = useHistory();
// 이 부분은 주석처리!
// console.log(props);
// const my_lists = props.list;
// 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.
// 우리는 그 중, bucket 안에 들어있는 list를 가져옵니다.
const my_lists = useSelector((state) => state.bucket.list);
return (
<ListStyle>
{my_lists.map((list, index) => {
return (
<ItemStyle
className="list_item"
key={index}
onClick={() => {
history.push("/detail");
}}
>
{list}
</ItemStyle>
);
})}
</ListStyle>
);
};
const ListStyle = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
`;
const ItemStyle = styled.div`
padding: 16px;
margin: 8px;
background-color: aliceblue;
`;
export default BucketList;
-1) 몇 번째 상세에 와있는 지 알기 위해, URL 파라미터를 적용하자
// App.js
<Route exact path="/detail/:index" component={Detail} />
//BucketList.js
...
{my_lists.map((list, index) => {
return (
<ItemStyle
className="list_item"
key={index}
onClick={() => {
history.push("/detail/"+index);
}}
>
{list}
</ItemStyle>
);
})}
...
-2) 상세페이지에서 버킷리스트 내용을 띄워보자
//Detail.js
// 리액트 패키지를 불러옵니다.
import React from "react";
// 라우터 훅을 불러옵니다.
import {useParams} from "react-router-dom";
// redux hook을 불러옵니다.
import { useSelector } from "react-redux";
const Detail = (props) => {
// 스토어에서 상태값 가져오기
const bucket_list = useSelector((state) => state.bucket.list);
// url 파라미터에서 인덱스 가져오기
const params = useParams();
const bucket_index = params.index;
return <h1>{bucket_list[bucket_index]}</h1>;
};
export default Detail;
하아... 리덕스... 흐름은 이해하겠는데 직접사용하기가 너무 힘들다... 머리가 너무 아프다 진짜 할수있을까 싶을정도다.
일딴 반복해서 계속 듣고 쓰고 있는데 익숙해지면 좋아질꺼라고 굳게 믿고 해본다...