[Redux] React + Redux App 만들기(생활코딩)
이번엔, React 컴포넌트와 Redux의 조합을 공부하려고 한다. (생활코딩 react-redux 과제와 함께)
Global State를 Redux 문법을 통해서 다루는 것 뿐만 아니라, 디렉토리와 파일구조(store, reducer 등)를 배우는 것에도 중점을 둬야겠다.
💜 React Without Redux 💀
이번엔, 아래와 같이 숫자를 수정하고 표시하는데 있어 각각 두 번의 Depth를 거치는 컴포넌트를 React로 만들었다.
// 1. AddNumber.js
const AddNumber = ({ plusNum }) => {
const [size, setSize] = useState(1);
return (
<div>
<h3>Add Number</h3>
<input type="text" value={size} onChange={(e) => {
setSize(Number(e.target.value));
}} />
<input type="button" value="+" onClick={() => plusNum(size)}/>
</div>
)
}
// 2. AddNumberRoot.js
const AddNumberRoot = ({ plusNum }) => {
return (
<div>
<h2>Add Number Root</h2>
<AddNumber plusNum={plusNum} />
</div>
)
}
// 3. App.js
function App() {
const [number, setNumber] = useState(0);
const plusNum = (size) => {
const newNum = number + size
setNumber(newNum);
}
return (
<div className="App">
<h1>React Redux: 생활코딩</h1>
<AddNumberCon plusNum={plusNum} />
<ShowNumberCon number={number} />
</div>
);
}
// 4. ShowNumberRoot.js
const ShowNumberRoot = ({ number }) => {
return (
<div>
<h2>Show Number Root</h2>
<ShowNumber number={number} />
</div>
)
}
// 5. ShowNumber.js
const ShowNumber = ({ number }) => {
return (
<div>
<h2>Show Number</h2>
<input type="text" value={number} />
</div>
)
}
숫자를 변경하고 이것이 표현될때까지 컴포넌트 흐름순으로 코드를 나열했다.
App.js에서 plusNum 함수를 props로 하달하고, AddNumber.js는 size(증가량) 상태를 포함해서 다시 props를 전달해야 한다.
또한, ShowNumber.js로 number 상태를 props로 하달해서 이를 표현시켜야 한다.
이 때, 중간의 Container 컴포넌트들은 상태를 활용하지는 않지만, 이를 전달하기 위해 props 수용/하달을 진행하는 문제가 있다.
또한, Depth가 깊어지면 그만큼 props Drilling을 위한 로직들이 추가되어야 한다.
💜 React + Redux 😃
- 설치(npm)
1) Redux 코어 설치: 리덕스 기본 라이브러리 설치
npm install redux
2) Redux Toolkit(RTK): Redux 공식추천 방법. 리덕스에 필요한 패키지와 함수를 번들러로 받을 수 있다.
npm install @reduxjs/toolkit
3) React Redux 앱 만들기: React의 CRA와 Redux Toolkit을 조합한 템플릿으로 초기세팅을 바로 할 수 있는 설치방법이다.
npx create-react-app [디렉토리명] --template redux
- 코드수정
1) Store.js : 스토어, 리듀서 함수 제작
import { createStore } from 'redux';
const reducer = (state, action) => {
if (!state) {
return {
number: 0,
}
}
if (action.type === 'INCREMENT') {
return {...state, number: state.number + action.size}
}
}
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
먼저, Store.js 파일을 만든다. 여기에는 Redux의 스토어와 리듀서 함수가 들어가 있다.
기존에, App.js에서 관리하던 number 상태와, 숫자를 증가시키는 로직을 이제 여기서 관리한다.
2) AddNumber.js : onClick(plusNum 함수) props 불필요, 버튼 클릭시 디스패치 발생
import React, { useState } from 'react';
import store from '../Store';
const AddNumber = () => {
const [size, setSize] = useState(1);
return (
<div>
<h3>Add Number</h3>
<input type="text" value={size} onChange={(e) => {
setSize(Number(e.target.value));
}} />
<input type="button" value="+" onClick={() => {
store.dispatch({
type: 'INCREMENT',
size,
})
}}/>
</div>
)
}
export default AddNumber
3) ShowNumber.js : number props 불필요, store.getState() 로 상태 불러오기
// ShowNumber.js
import React, { useState } from 'react';
import store from '../Store';
const ShowNumber = () => {
const [number, setNumber] = useState(0);
store.subscribe(() => {
setNumber(store.getState().number)
})
return (
<div>
<h2>Show Number</h2>
<input type="text" value={number} />
</div>
)
}
export default ShowNumber
4) AddNumberRoot.js, ShowNumberRoot.js 등 컴포넌트들의 props 전달이 불필요해짐
- Container 컴포넌트 & Presentationl 컴포넌트
위 AddNumber.js 컴포넌트를 예시로 보자. 이 컴포넌트는 재활용이 까다롭다. (리덕스 스토어에 종속되어 있기 때문)
만약, 이 컴포넌트의 UI를 재활용하려고 하는데, 내부의 값은 Global State와 무관할 경우 코드를 복붙해야만 할 것이다.
재활용성 향상을 위해, 우리는 컴포넌트를 기능적으로 분리한다.
- Container 컴포넌트: Redux 관련기능만을 담당한다. 부모 컴포넌트로서 Presentational 을 감싸며, props를 내려준다.
- Presentational 컴포넌트: Redux 기능은 배제되고 UI만 담당한다. 자식 컴포넌트로 Container의 props 값을 활용한다.
// Container: AddNumberCon.js
import React from 'react'
import AddNumber from '../components/AddNumber';
import store from '../Store';
const AddNumberCon = () => {
return (
<AddNumber onClick={(size) => {
store.dispatch({
type: 'INCREMENT',
size,
})
}}/>
)
}
// Presentational: AddNumber.js
import React, { useState } from 'react';
const AddNumber = ({ onClick }) => {
const [size, setSize] = useState(1);
return (
<div>
<h3>Add Number</h3>
<input type="text" value={size} onChange={(e) => {
setSize(Number(e.target.value));
}} />
<input type="button" value="+" onClick={() => onClick(size)}/>
</div>
)
}
Container인 <AddNumberCon> 를 만든다. Redux와 관련된 디스패치 함수가 포함되며, props로 <AddNumber>에 내려준다.
<AddNumber> 는 Presentational 을 맡는다. props로 받은 onClick 함수(디스패치)에, 현재 size값만 넘겨주면 된다. (No Redux)
분명, Redux 를 통한 상태관리와 props Drilling 제거는 좋은 현상이다.
하지만, 마지막처럼 우리는 컴포넌트의 기능분리를 위해 Container & Presentational 로 각각 나눠주어야 했다.
또한, 규모가 커질수록 이 역시 이중작업이 되며, 다루는 상태가 많아질수록 Container가 내리는 props도 증가한다.
이러한 한계에 다시 봉착하면서, React와 Redux가 각각의 역할에 좀 더 충실하도록 해주는 것이 바로 react-redux 라이브러리다.
다음 포스팅에선, 이 react-redux에 대해 좀 더 다뤄보겠다.