*목차
- 소개
- 예제
- 요구사항에 따른 Props drilling 발생
- redux 설치
- redux 적용 예제
* React, React hook에 대한 사전 지식이 필요합니다.
* 작업 환경은 아래를 참고해 주세요
소개 (Introduce)
React는 Web application을 만들때 거의 필수적으로 쓰이는 Library가 되었습니다. 하지만 Web Application의 복잡도가 올라가면 올라갈 수록 React만으로는 상태관리가 어렵고, 성능을 최적화 하기 어려워 집니다.
필자의 경우는 이전에 React로 Web Application을 만들었을 때 수많은 데이터 상태관리와 API의 동기화를 필요로 하는 요구사항을 구현하면서 React 만으로는 한계가 있음을 느꼈습니다. 이런 복잡도에서는 중앙 상태관리 체제가 필요하다 판단하여 모든 Data를 상위 Component로 옮겼습니다. 그러자 Props drilling등 다양한 최적화 문제와 관련된 문제가 나타나게 되었습니다. 이런 문제를 Redux는 어떻게 해결할 수 있었을까요?
* 혹시 Props drilling 문제에 대해서 Context API를 쓰는 것이 어떻냐는 의견이 있을 수 있습니다. 불가능한 것은 아니지만 이 글의 주된 목적은 redux를 쉽게 활용할 수 있도록, 그리고 그 활용의 이유를 쉽게 이해할 수 있는 Tutorial 성격이 강합니다.
* Context API와 redux가 어떻게 다른지 알려주는 좋은 글이 있어 소개를 합니다
간단한 예제를 통해 Props drilling의 문제 재현, 그리고 해결 방법을 알아봅시다.
예제 (Example)
간단한 Web App을 작성해 보겠습니다. App Component 안에 Select 기능을 가지고 있는 Component를 만들어 보겠습니다. 구조는 아래 그림과 같습니다.
* src > components > ComponentSelect5.jsx
import { useState } from "react";
export default function ComponentSelect5({}) {
const dataArray = [1, 2, 3, 4, 5];
const [selectedValue, setSelectedValue] = useState(dataArray[0]);
const handleSelectChange = (event) => {
setSelectedValue(event.target.value);
};
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
<h2>Select a Number</h2>
<select name="data" value={selectedValue} onChange={handleSelectChange}>
{dataArray.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<span> You selected : {selectedValue}</span>
</div>
</>
);
}
dataArray는 select의 선택목록입니다. 이 Component는 selectedValue를 변수로 하여 상태관리를 하고 있습니다. 이 Component가 Home에 있을 때는 전혀 문제가 없어 보입니다.
* src > App.jsx
import ComponentSelect5 from "./components/ComponentSelect5";
import "./App.css";
function App() {
return (
<>
<ComponentSelect5 />
</>
);
}
export default App;
Web을 실행하면 다음과 같이 확인할 수 있습니다.
Component는 정상작동합니다. 사실 이정도면 더이상 수정할 게 없는 App이라 할 수 있지요. 그런데 이 ComponentSelect5가 하위 Component로 가게 된다면 어떤 일이 벌어질까요? Component가 5개 정도 연달아 하위구조를 가진 형태로 만들어 보겠습니다.
* src > App.jsx
import "./App.css";
import Component1 from "./components/Component1";
function App() {
return (
<>
<Component1 />
</>
);
}
export default App;
* src > components > Component1.jsx ~ Component3.jsx
import Component2 from "./Component2";
export default function Component1() {
const random_number = Math.random().toFixed(4);
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
Component1 random number : {random_number}
<Component2 />
</div>
</>
);
}
* src > components > Component4.jsx
import ComponentSelect5 from "./ComponentSelect5";
export default function Component4() {
const random_number = Math.random().toFixed(4);
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
Component4 random number : {random_number}
<ComponentSelect5 />
</div>
</>
);
}
*src > components > ComponentSelect5.jsx
import { useState } from "react";
export default function ComponentSelect5() {
const dataArray = [1, 2, 3, 4, 5];
const [selectedValue, setSelectedValue] = useState(dataArray[0]);
const handleSelectChange = (event) => {
setSelectedValue(event.target.value);
};
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
<h2>Select a Number</h2>
<select name="data" value={selectedValue} onChange={handleSelectChange}>
{dataArray.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<span> You selected : {selectedValue}</span>
</div>
</>
);
}
이렇게 코드를 작성하면 다음과 같은 결과물을 얻을 수 있습니다. random number는 component의 re-rendering이 일어나는지 확인을 위한 숫자입니다.
지금의 App에서 Select Number의 상태관리는 ComponentSelect5에서 관리되고 있기 때문에 Component1 ~ Component4의 random number에는 전혀 영향을 미치지 않습니다. 새로고침 할때서야 전체 App이 re-rendering되면서 random number가 바뀌는 것을 확인할 수 있습니다.
요구사항 추가에 따른 Props drilling 발생
이제 여기서부터가 문제입니다. 저 You selected : {selectedValue}에서 보이던 selectedValue가 App Component에서도 출력하고 싶다는 요구사항이 추가된다면 어떻게 될까요? useState로 관리되고 있는 selectedValue값을 상위 Component에서 받아올 방법이 없으므로 selectedValue 자체를 상위 Component로 옮기는 수 밖에 없습니다. 그리고 상위 Component에서 관리되는 selectedValue를 Component를 통해 아래로 내려야 합니다.
이렇게 되면 기능동작으로는 문제가 없어 보입니다. 하지만 Select number를 전달하는 Component1 ~ Componen4는 어떻게 될까요? 우선 코드로 구현하면 다음과 같습니다.
* src > App.jsx
- useSelect를 App Component에서 생성하여 하위 Component로 전달한다.
import { useState } from "react";
import Component1 from "./components/Component1";
import "./App.css";
function App() {
const dataArray = [1, 2, 3, 4, 5];
const [selectedValue, setSelectedValue] = useState(dataArray[0]);
return (
<>
<div>
<h3>You selected : {selectedValue}</h3>
<Component1
dataArray={dataArray}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
/>
</div>
</>
);
}
export default App;
* src > components > Component1.jsx ~ Component3.jsx
import Component2 from "./Component2";
export default function Component1({
dataArray,
selectedValue,
setSelectedValue,
}) {
const random_number = Math.random().toFixed(4);
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
Component1 random number : {random_number}
<Component2
dataArray={dataArray}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
/>
</div>
</>
);
}
* src > components > Component4.jsx
import ComponentSelect5 from "./ComponentSelect5";
export default function Component4({
dataArray,
selectedValue,
setSelectedValue,
}) {
const random_number = Math.random().toFixed(4);
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
Component4 random number : {random_number}
<ComponentSelect5
dataArray={dataArray}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
/>
</div>
</>
);
}
* src > components > ComponentSelect5.jsx
- useState가 사라지고 대신 props로 받아와서 같은 동작을 수행한다.
export default function ComponentSelect5({
dataArray,
selectedValue,
setSelectedValue,
}) {
const handleSelectChange = (event) => {
setSelectedValue(event.target.value);
};
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
<h2>Select a Number</h2>
<select name="data" value={selectedValue} onChange={handleSelectChange}>
{dataArray.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<span> You selected : {selectedValue}</span>
</div>
</>
);
}
이렇게 코드를 작성하면 다음과 같은 동작을 수행합니다. 요구사항이었던 App Component에서 You selected : {number} 가 제대로 동작하는 것을 확인할 수 있습니다. 그리고 select number가 변경될 때마다 random number또한 변경되는 것 또한 확인할 수 있습니다.
이것은 Component가 refresh되고 있다는 증거입니다. (만약 여러분이 React dev tool을 쓰시고 계시다면 더욱 쉽게 확인하실 수 있습니다.) 그리고 이런 불필요한 re-rendering은 React로 만든 App의 성능을 저하시키는 원인이 됩니다. 이렇게 상위에서 하위 Component로 Props를 전달하면서 불필요한 re-rendering이 이뤄지는 현상을 Props drilling이라 합니다.
지금은 random number를 요청하는 수준의 가벼운 작업이기에 큰 성능저하를 느낄 수 없지만 만약에 re-rendering되면서 useEffect가 동작해 api를 수시로 호출하는 경우에는 확실한 차이를 느끼실 수 있습니다.
이런 Props drilling 문제를 우리는 어떻게 해결할 수 있을까요? 단순하게 생각했을 때는 App에서 ComponentSelect5로 바로 전달 할 수 있다면 해결될 문제처럼 보입니다.
하지만 이렇게 하면 여전히 App이 update되면서 자식 Component는 update되는 현상이 일어납니다. 그러므로 App에서 select number를 보여주는 기능을 따로 빼주는 것이 적절합니다.
하지만 어떻게 해야 ComponentSelect5에서 변경한 상태를 TitleComponent에서 인식해서 반영할 수 있을까요? 이런 동작은 React에서는 좀 난감한 부분입니다. App에서 useState를 관리하면 componen1~4가 불필요한 re-rendering을 일으키고, Title Component와 ComponentSelect5를 연동하자니 하위 Component 구조가 아니라서 전달하기 어렵습니다.
Redux는 이런 상황에서 매우 효과적입니다. Redux를 사용해서 ComponentSelect5에서 상태변경된 data를 TitleComponent에서 인식해서 update되도록, 그리고 불필요한 re-rendering이 일어나지 않도록 해봅시다.
Redux 설치
terminal을 열고 package를 설치합시다.
yarn add @reduxjs/toolkit
yarn add react-redux
이 패키지 설치에는 2가지 의문이 있을 수 있습니다.
1. 왜 yarn add redux를 안했을까요?
→ 공식문서의 권장사항이기 때문입니다.
2. 왜 redux toolkit(줄여서 RTK라고도 합니다.)과 react-redux를 왜 따로 설치할까요?
→ redux는 react만을 위한 library가 아니기 때문입니다. Javascript로 만들어진 application 전체를 대상으로 하는 redux는 vue.js, angular.js등 다양한 Javascript 기반 Application에서 상태관리를 지원합니다.
→ react-redux는 React의 구조에 맞게 쉽게 사용하도록 돕는 library를 지원합니다.
그래서 RTK가 지원하는 부분(Store, reducer 등)과 react-redux가 지원하는 부분(provider, react hook)가 다릅니다. 이런 부분을 혼동하지 말고 잘 작성해 봅시다.
Redux 적용 예제
Redux는 상태관리 library라고는 하지만 framework처럼 패턴을 지켜줘야 원활하게 사용이 가능합니다.
여기서 필자가 알려드리는 영역은 크게 5가지로 구성되어 있습니다.
1. 먼저 데이터 상태를 보관(+관리)할 Store
2. Store에 있는 data를 어디까지 제공할지 범위(보통은 App 전체)를 정하는 Provider
3. Store에 있는 data를 어떻게 변경할지 logic을 정하는 Reducer
4. Store에 있는 data의 state를 변경 요청할 Dispatch
5. Store에 있는 data를 꺼내서 사용할 Selector
밑의 그림을 통해 Store, Provider, Reducer, dispatch, Selector가 어느 위치에서 어떤 역할을 하는지 확인해 볼 수 있습니다.
이 5가지에 대한 용어가 자연스럽게 다가와야 redux를 편하게 사용할 수 있습니다. 좀 더 일반적인 예시를 드리겠습니다.
1. 물품들를 쌓아서 판매, 보관 등을 하는 공간을 제공하는 가게(Store)
2. 가게에 있는 물품들을 어느 지역까지 판매를 할지 정하는 공급자(Provider)
3. 가게에 있는 물품들을 요청자들의 요청에 따라 구매 요청, 판매 요청에 따라 물품을 다루는 점원(Reducer)
4. 가게에 있는 물품을 구매 요청, 판매 요청을 하는 요청자(Dispatch)
5. 가게에 있는 물품들 중 특정 물품을 선택해서 현황을 확인하는 선택자(Selector)
이런 개념으로 보시면 됩니다. 하나씩 만들어가면서 설명 드리겠습니다.
1. Store 작성
먼저 Store를 만들어 보겠습니다. src폴더에 store.js 파일을 생성 후 작성했습니다.
* src > store.js
import { configureStore } from "@reduxjs/toolkit";
export default configureStore({
reducer: {},
});
store에 data를 변경할 logic을 담당할 reducer는 아직 빈칸입니다. 나중에 채워보도록 하겠습니다.
2. Provider 작성
앞에서 만든 Store를 App component에 적용하기 위해 Provider를 생성해 보도록 하겠습니다. 저는 main.jsx가 react의 기본 root component이므로 이 파일에 Provider를 작성했습니다.
configureStore를 store로 import하는 것에 유의합시다.
* src > main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App.jsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
3. Reducer 생성
reducer는 src/slices/ 폴더를 생성 후 그 안에 만들어주도록 합시다. 필자는 reducer 파일명을 selectedDataSlice.js로 지었습니다.
* src > slices > selectedDataSlice.js
import { createSlice } from "@reduxjs/toolkit";
export const initDataArray = [1, 2, 3, 4, 5];
export const selectedDataSlice = createSlice({
name: "selectedData",
initialState: {
value: initDataArray[0],
},
reducers: {
change: (state, action) => {
state.value = action.payload;
},
},
});
export const { change } = selectedDataSlice.actions;
export default selectedDataSlice.reducer;
reducer를 작성하려 했는데 왜 Slice라는 이름이 갑자기 튀어나온 걸까요? Slice는 data의 초기 상태 정의, data 상태를 조절하기 위한 Action 정의, 실제로 state를 최신화 해줄 reducer등이 합쳐진 상태입니다. redux만을 설치헀을 때는 reducer, initstate, action을 전부 따로 작성해야 했으나 지금은 RTK를 활용하여 slice만으로 쉽게 작성이 가능합니다.
이 slice를 store에 연결해 줍시다.
* src > store.js
import { configureStore } from "@reduxjs/toolkit";
import selectedDataReducer from "./slices/selectedDataSlice";
export default configureStore({
reducer: {
selectedData: selectedDataReducer,
},
});
4. Dispatch, Selector 작성
dispatch는 Data를 변경할 때 사용하고, Selector는 Data의 값을 참조할 때 사용합니다.
ComponentSelect5에서는 Data를 변경하고 참조하는 것을 둘 다 하기 때문에 dispatch와 selector를 다 작성하겠습니다.
* src > components > ComponentSelect5.jsx
import { useDispatch, useSelector } from "react-redux";
import { change, initDataArray } from "../slices/selectedDataSlice";
export default function ComponentSelect5() {
const handleSelectChange = (event) => {
dispatch(change(event.target.value));
};
const selectedValue = useSelector((state) => state.selectedData.value);
const dispatch = useDispatch();
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
<h2>Select a Number</h2>
<select name="data" value={selectedValue} onChange={handleSelectChange}>
{initDataArray.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<span> You selected : {selectedValue}</span>
</div>
</>
);
}
App.jsx에서는 Title Component를 추가하고 Hook을 전부 없애겠습니다.
* src > App.jsx
import Component1 from "./components/Component1";
import TitleComponent from "./components/TitleComponent";
import "./App.css";
export default function App() {
return (
<>
<div>
<TitleComponent />
<Component1 />
</div>
</>
);
}
Title Component는 Selector를 사용합니다.
* src > components > TitleComponent.jsx
import { useSelector } from "react-redux";
export default function TitleComponent() {
const selectedValue = useSelector((state) => state.selectedData.value);
return (
<>
<h3>You selected : {selectedValue}</h3>
</>
);
}
중간 단계에 있던 Component1~4.jsx에서 useState를 전달하던 동작을 제거합니다.
* src > components > Component1.jsx ~ Component3.jsx
import Component2 from "./Component2";
export default function Component1() {
const random_number = Math.random().toFixed(4);
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
Component1 random number : {random_number}
<Component2 />
</div>
</>
);
}
* src > components > Component4.jsx
import ComponentSelect5 from "./ComponentSelect5";
export default function Component4() {
const random_number = Math.random().toFixed(4);
return (
<>
<div style={{ border: "1px solid grey", margin: "10px", padding: "4px" }}>
Component4 random number : {random_number}
<ComponentSelect5 />
</div>
</>
);
}
이제 코드를 다 작성했습니다. 제대로 동작하는지 확인해 볼까요?
이제는 Component1~4는 re-rendering이 되지 않고 상단의 You selected가 정상적으로 바뀌는 것을 확인할 수 있습니다.
마치며...
이전에 React를 처음 시작할 때는 redux는 너무 무거운 tool이니 되도록이면 context API를 사용하라는 얘기를 들었습니다. 하지만 redux에 익숙해 지면 App이 동작할 수 있는 범위를 크게 넓일 수 있습니다. 이번 예제는 props drilling에 초점을 맞추어 글이 작성이 되었지만 redux는 이런 문제 외에도 다양한 문제들을 풀어낼 수 있는 좋은 상태관리 library입니다. 이 글을 읽은 여러분들이 이번 기회에 Project에 redux를 도입하는 계기가 되었으면 합니다. 감사합니다.
* reference
https://ko.redux.js.org/tutorials/quick-start
https://ko.redux.js.org/introduction/why-rtk-is-redux-today/
'React > Basic' 카테고리의 다른 글
React Hook - useEffect (0) | 2023.08.02 |
---|---|
React Hook - useReducer (0) | 2023.07.24 |
React Hook - useState (0) | 2023.07.23 |
React Hook 알아보기 (0) | 2023.07.23 |
Material UI - Tutorial (0) | 2023.07.09 |