본격적으로 본론에 들어가기 전에,
npm trends를 살펴보면 상태 관리 라이브러리 중에서도
redux(하늘색)가 가장 많이 쓰이고 있는 것을 알 수 있다.
따라서, 비교적 간편한 Redux toolkit을 쓰기 전에, 기존 Redux 코드를 작성해보며
데이터 흐름을 숙지하고, 개념을 확실히 이해하고 넘어가야 할 필요가 있다.
장바구니 추가, 삭제 등 기본적인 쇼핑몰 기능을 구현한 이전 포스팅 에서 더 나아가,
두 번째 포스팅에서는 해당 앱에서 구현한 또 다른 Reducer인 notificationReducer 등에 대해 다뤄보겠다.
결과 화면
- 장바구니에 없는 물건을 '장바구니 담기'를 눌렀을 때 -> "장바구니에 ~~이(가) 추가되었습니다." 메시지가 뜬다.
- 장바구니에 이미 있는 물건을 또 '장바구니 담기'를 눌렀을 때 -> "이미 추가된 상품입니다." 메시지가 뜬다.
Redux 사용하기
1. Store 생성
:전역 상태 저장소 Store는 만들어진 상태니, 액션 생성자 함수를 만드는 것부터 시작한다.
2. Action 생성자 함수 생성
export const NOTIFY = "NOTIFY";
export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION";
export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION";
export const notify = (message, dismissTime = 5000) => dispatch => { // 🟣 (1)
const uuid = Math.random()
dispatch(enqueueNotification(message, dismissTime, uuid))
setTimeout(() => {
dispatch(dequeueNotification())
}, dismissTime)
}
// 메시지 생성
export const enqueueNotification = (message, dismissTime, uuid) => { // 🟣 (2)
return {
type: ENQUEUE_NOTIFICATION,
payload: { message, dismissTime, uuid }
}
}
// 메시지 삭제
export const dequeueNotification = () => {
return {
type: DEQUEUE_NOTIFICATION
}
}
🟣 (1)의 코드는 Redux의 thunk 액션 생성자 함수로, 비동기 작업을 처리하기 위해 사용된다.
---> notify 함수는 2개의 매개변수를 받아서 함수를 반환하고, 이 반환된 함수는 dispatch 메서드를 매개변수로 받아 사용한다.
Redux thunk에서 dispatch 된 액션이 함수인지 아닌지 판단한다.
Redux Thunk -> 함수인가? -> 아니요.(Plain Object) -> dispatch -> Reducer
Redux Thunk -> 함수인가? -> 예 -> 함수실행 -> dispatch -> 함수인가? -> 아니요(Plain Object) -> Reducer
[일반적인 형태]
export const fetchData = () => async dispatch =>{
const response = await APIadress.get('/data');
dispatch({ type: 'FETCH_DATA', payload: response }) }
- notify 함수는 message와 dismissTime이라는 두 개의 매개변수를 받는다.
- message는 알림 메시지를 나타내며, dismissTime은 알림이 표시되는 시간(밀리초)을 나타낸다.
- dismissTime이 지난 후 알림이 자동으로 사라진다. (dismissTime이 생략된 경우, 기본값으로 5000ms(5초)가 지정)
- notify 함수는 thunk 함수이므로, Redux의 dispatch 함수를 매개변수로 받는다.
- 이 함수를 통해 액션을 디스패치(dispatch)하고, 액션 객체를 반환하는 함수를 반환한다.
- notify 함수 내부에서는 먼저, Math.random()을 사용하여 랜덤한 UUID(Universally Unique Identifier)를 생성한다.
- 그리고 enqueueNotification 액션 생성자 함수를 호출하여 알림을 큐(queue)에 추가하고, 생성된 UUID를 액션 객체에 추가한다. 이렇게 하면 알림이 표시되는 순서를 유지할 수 있다.
- 그 다음, setTimeout 함수를 사용하여 dismissTime이 지난 후에 dequeueNotification 액션을 디스패치하여 큐에서 알림을 제거한다.
- 즉, notify 함수는 알림 메시지를 큐에 추가하고, 일정 시간이 지난 후에 알림을 자동으로 사라지게 하는 기능을 수행한다.
3. Reducer 작성
notificationReducer.js
import { ENQUEUE_NOTIFICATION, DEQUEUE_NOTIFICATION } from "../actions/index";
const notificationReducer = (state = {notifications:[]}, action) => {
switch (action.type) {
case ENQUEUE_NOTIFICATION: // Object.assign을 사용한 첫 번째 방법
return Object.assign({}, state, {
notifications: [...state.notifications, action.payload]
});
// spread syntax를 사용한 두 번째 방법 (+ ...state가 딱히 필요한 상황이 아니므로 리팩토링)
// { notifications: [...state.notifications, action.payload] };
case DEQUEUE_NOTIFICATION:
return {
...state, // 이전 객체를 참조하지 않아도 되는 경우이므로 이 줄은 생략 가능
notifications: state.notifications.slice(1) // 알림 목록에서 가장 오래된 알림을 제거
};
default:
return state;
}
}
export default notificationReducer;
- notificationReducer 함수는 초기 상태 값으로 notifications 배열을 포함하는 객체를 가진다.
- notifications 배열은 {message, dismissTime, uuid}가 담긴, 알림 메시지를 나타내는 객체들을 담는다.
- 첫 번째 case에서는 ENQUEUE_NOTIFICATION 액션이 발생했을 때의 동작을 정의한다. action.payload는 액션 객체에 추가로 담긴 데이터를 나타내며, 여기서는 새로운 알림 메시지 객체 {message, dismissTime, uuid} 를 담고 있다. notifications 배열에 새로운 알림 메시지 객체를 추가한 후 새로운 상태 객체를 반환한다.
- 두 번째 case에서는 DEQUEUE_NOTIFICATION 액션이 발생했을 때의 동작을 정의한다. state.notifications.slice(1)은 notifications 배열의 첫 번째 요소를 제외한 나머지 요소들을 담은 새로운 배열을 반환한다.
- 마지막 default 문은 action.type에 일치하는 케이스가 없을 때 기존 상태를 반환하는 역할을 한다.
4. Dispatch 적용
import React from 'react';
import { addToCart, notify } from '../actions/index'; // notify를 import
import { useSelector, useDispatch } from 'react-redux';
import Item from '../components/Item';
function ItemListContainer() {
const state = useSelector(state => state.itemReducer);
const { items, cartItems } = state;
const dispatch = useDispatch();
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
dispatch(addToCart(item.id))
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`)) // 장바구니에 해당 상품이 없을 때,
}
else {
dispatch(notify('이미 추가된 상품입니다.')) // 장바구니에 해당 상품이 있을 때
}
}
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">쓸모없는 선물 모음</div>
{items.map((item, idx) => <Item item={item} key={idx}
handleClick={handleClick} />)}
</div>
</div>
);
}
export default ItemListContainer;
5. 메시지 창 components
NotificationCenter.js (부모)
import { useSelector } from 'react-redux';
import Toast from './Toast';
function NofiticationCenter() {
const state = useSelector(state => state.notificationReducer);
return <div className="notification-container top-right">
{
state.notifications.map((n) =>
<Toast key={n.uuid} text={n.message} dismissTime={n.dismissTime} /> <!-- 알림창 컴포넌트 -->
)
}
</div>
}
export default NofiticationCenter
- 이 코드는 Redux store에서 상태를 가져와 state 변수에 할당하고, notifications 배열을 매핑하여 Toast 컴포넌트를 렌더링하는 NotificationCenter 컴포넌트를 정의한다.
- 알림을 관리하는 데 필요한 모든 구성 요소를 포함하고 있으며, 알림을 표시하는 데 필요한 모든 데이터와 로직을 관리한다.
- useSelector 훅을 사용하여 Redux store에서 notificationReducer의 상태를 가져온다.
- notifications 배열을 매핑하여 Toast 컴포넌트를 렌더링한다. key 속성으로 고유한 uuid를 사용한다.
- NotificationCenter 컴포넌트는 notification-container 클래스를 사용하여 CSS 스타일을 적용하고, top-right 클래스를 사용하여 오른쪽 상단에 알림을 표시한다.
Toast.js (자식)
import React, { useEffect, useState } from 'react'
export default function Toast({ text, dismissTime }) {
const [isFading, setIsFading] = useState(false)
useEffect(() => {
let mounted = true
setTimeout(() => {
if (mounted) { setIsFading(true) } }, dismissTime - 500) // dismissTime이 5초라면 4.5초 뒤에 실행
return () => { mounted = false }
}, [])
return (
<div className={`notification ${isFading ? 'fade-out' : ''}`}>
{text}
</div>
)
}
- Toast 컴포넌트는 전달된 text와 dismissTime props를 사용하여 알림을 표시하고, isFading state를 사용하여 알림을 페이드 아웃 처리한다.
- Toast 컴포넌트는 text와 dismissTime 두 개의 프롭스(props)를 받는다.
(text 프롭스는 알림 메시지를 나타내는 문자열이며, dismissTime 프롭스는 알림이 표시되는 시간(밀리초)을 나타내는 숫자) - 컴포넌트 내부에서는 React의 useState 훅을 사용하여 isFading 상태를 관리한다.
isFading은 알림이 페이드아웃(fade-out) 중인지를 나타내는 불리언(boolean) 값이다. - 그리고 useEffect 훅을 사용하여 알림이 표시된 후 일정 시간이 지나면 isFading 값을 true로 변경하여 알림이 페이드아웃되도록 한다. 이 때, setTimeout 함수를 사용하여 일정 시간이 지난 후 setIsFading(true) 함수가 호출되도록 한다.
- 마지막으로, return 구문에서는 알림 컴포넌트가 마운트되어 있는 동안에만 setTimeout 함수가 실행되도록 하고, 컴포넌트가 언마운트될 때 mounted 값을 false로 설정하여 setTimeout 함수가 실행되지 않도록 한다.
- Toast 컴포넌트는 text와 dismissTime 두 개의 프롭스(props)를 받는다.
관련 CSS
.notification {
transition: transform 0.6s ease-in-out;
animation: toast-in-right 0.6s;
background: var(--coz-purple-600);
transition: 0.3s ease;
border-radius: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
color: #000;
opacity: 0.9;
font-weight: 600;
height: 50px;
width: 360px;
color: #fff;
padding: 15px;
margin: 10px;
}
.fade-out {
opacity: 0;
transform: opacity 2s;
}
- notification 클래스는 알림의 기본 스타일을 정의한다.
- fade-out 클래스는 알림이 페이드아웃되는 동안 적용될 스타일을 정의한다.
기타 깨달음의 메모
redux devTools
콘솔에 찍어 보는 것처럼, 상태 관리도 브라우저에서 확인할 수 있어서 유용한 크롬 익스텐션이다.
아래 그림처럼 별개의 창으로 띄워 쓸 수도 있고
개발자 코드에서 바로 사용도 가능하다.
React 앱 렌더링 방식
첫 번째 코드 (React 18 이상의 버전의 최신 방식)
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<Provider store={store}>
<App />
</Provider>
);
- createRoot API를 사용하여 React 18의 Concurrent Mode를 활용한 코드이다. 이를 사용하면 React가 더욱 효율적으로 렌더링을 처리할 수 있다.
- createRoot를 사용하여 렌더링된 요소는 애플리케이션의 전체 트리에 대한 렌더링 중인 요소로 처리된다. 이것은 병렬로 처리할 수 있는 모든 작업을 최대한 병렬로 처리하여 애플리케이션의 성능을 향상시키는 데 도움이 된다.
두 번째 코드 (React 17 이하의 버전)
import React from 'react';
import ReactDOM from 'react-dom';
.
.
.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
- React 17 이하의 버전에서 사용되는 코드이다. 이 코드는 ReactDOM.render를 사용하여 애플리케이션을 렌더링한다.
- 결론은, React 18 이상의 버전에서는 첫 번째 코드를 사용하는 것이 더욱 효율적이다. 그러나 React 17 이하의 버전에서는 두 번째 코드를 사용해야 한다.
serviceWorker?
최상위 index.js에 이런 문구가 적혀 있어, 뜻을 찾아보았다.
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
서비스 워커는 브라우저에서 백그라운드에서 실행되는 스크립트로, 웹 애플리케이션의 네트워크 요청을 가로채고 캐싱하여 오프라인 상태에서도 작동할 수 있도록 한다.
- serviceWorker.unregister()는 React 애플리케이션에서 서비스 워커를 등록하지 않도록 해주는 코드이다.
- create-react-app에서는 기본적으로 서비스 워커가 등록되어 있어, 애플리케이션이 처음 로드될 때 서비스 워커가 설치된다.
- 그러나, serviceWorker.unregister()를 사용하면 서비스 워커가 등록되지 않도록 하여 사용자가 불필요한 캐시를 사용하지 않도록 할 수 있다.
- 만약 서비스 워커를 등록하고자 한다면, serviceWorker.register()를 사용하면 된다.
- 이렇게 함으로써 웹 애플리케이션을 오프라인 상태에서도 사용할 수 있으며, 사용자 경험을 향상시킬 수 있다.
- 서비스 워커는 네트워크 요청을 가로채서 캐싱하여, 애플리케이션의 성능을 향상시킨다.
- 하지만 다음과 같은 단점이 있다.
- 서비스 워커는 네트워크 요청을 가로채서 캐싱하여, 애플리케이션의 성능을 향상시킨다. 그러나, 서비스 워커가 캐시를 사용하게 되면, 사용자가 최신의 데이터를 보지 못하는 문제가 발생할 수 있다.
- 서비스 워커는 애플리케이션에 추가적인 코드를 추가하므로, 애플리케이션의 용량을 증가시킨다.
- 디버깅 용이성 서비스 워커는 네트워크 요청을 가로채므로, 디버깅이 어렵다.
- 결론적으로, 서비스 워커를 사용하지 않아도 되는 경우에는 서비스 워커를 등록하지 않는 것이 좋다.
ESLint 룰 검사?
useEffect 함수 안에 주석
useEffect(() => {
let mounted = true
setTimeout(() => {
if (mounted) { setIsFading(true) } }, dismissTime - 500)
return () => { mounted = false }
// eslint-disable-next-line react-hooks/exhaustive-deps // 🟣 해당 주석에 관한 설명
}, [])
- eslint-disable-next-line 주석은 ESLint 룰 검사를 잠시 동안 끄기 위해 사용되는 주석이다.
- React의 useEffect 훅에서는 종종 해당 훅이 의도한 대로 작동하지 않을 때 발생하는 경고 메시지가 있다.
- 이 경우 eslint-disable-next-line react-hooks/exhaustive-deps 주석을 사용하여 해당 경고를 무시할 수 있다.
- react-hooks/exhaustive-deps 는 React 훅에서 의존성 배열이 충분하지 않을 때 경고를 발생시키는 ESLint 룰 중 하나이다.
- 이를 해결하기 위해서는 useEffect 훅에서 사용하는 변수를 의존성 배열에 추가하거나, 훅의 두 번째 매개변수로 빈 배열을 전달하여 훅이 마운트된 후 한 번만 실행되도록 설정해야 한다.
Redux로 여러 상태 관리?
Redux로 여러개의 상태를 관리해야 하는 경우엔 다음과 같은 방법이 있다.
1. 하나의 reducer에 객체 형태로 상태를 관리하기
- 초기 상태값 객체 안에 여러 키를 넣어 관리하는 방법이다.
initialState.js
export const initialState =
{
"items": [
{
"id": 1,
"name": "노른자 분리기",
"img": "../images/egg.png",
"price": 9900
},
.
.
.
],
"cartItems": [
{
"itemId": 1,
"quantity": 1
},
.
.
.
]
}
itemReducer.js
import { initialState } from "./initialState";
const itemReducer = (state = initialState, action) => {
.
.
.
2. 여러 개의 reducer로 상태 관리하기
- 필요한 상태만큼 reducer를 만들어 관리하는 방법이다.
- 이렇게 여러개의 reducer가 있는 경우 combineReducer 메서드로 합친 다음 store를 생성할 때 전달해준다.
index.js
import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';
const rootReducer = combineReducers({ // 2개의 Reducer을 합쳤다.
itemReducer,
notificationReducer
});
export default rootReducer;
store.js
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from '../reducers/index';
.
.
const store = configureStore({
reducer: rootReducer,
.
.
여러 개의 reducer를 만들었을 때, dispatch가 발생하면 모든 reducer가 action을 전달받는다.
- 모든 reducer가 응답하는 것이 성능에 좋지 않다고 판단되면 redux-ignore, reduxr-scoped-reducer와 같은 모듈을 사용해서 특정 리듀서에만 전달해주는 방법도 있지만, 보통의 경우에는 성능 저하가 일어나지 않는다고 보기 때문에 굳이 사용하지는 않는다고 한다.
- 링크 참고 :
https://redux.js.org/faq/performance#wont-calling-all-my-reducers-for-each-action-be-slow
Performance | Redux
'FE > React' 카테고리의 다른 글
[React] Virtual DOM (0) | 2023.05.19 |
---|---|
[JS] useEffect 용례 (0) | 2023.05.04 |
쇼핑몰 앱에서 Redux를 이용한 상태 관리 (1) (0) | 2023.04.26 |
Redux로 간단한 Count 기능 구현하기 (0) | 2023.04.24 |
상태 관리 라이브러리 Redux (0) | 2023.04.24 |