counter(카운터) 앱을 만들어보면서 리덕스 기초를 익혀보자.
기능 설계
리덕스로 상태를 관리하고 이벤트가 발생할 때마다 변경된 상태를 화면에 띄워주는 기능을 구현할 것이다.
check list
+ 버튼으로 카운트 증가
- 버튼으로 카운트 감소
reset 버튼을 누르면 초기 상태로 되돌아가기
기본 세팅
리덕스 설치
아래 명령어로 redux와 관련된 패키지들을 설치하자.
$ yarn add redux react-redux @reduxjs/toolkit
앱 구성
하나의 파일에 리덕스 관련 코드를 다 넣어도 상관없지만 가독성을 위해 파일을 분리해 주었다.
📂 counter ├── public │ ├── index.html └── src │ ├── action.js │ ├── Counter.js │ ├── index.js │ ├── reducer.js │ ├── store.js
기능 구현
메인 컴포넌트
메인 컴포넌트가 될 Counter.js
에 다음과 같이 기본 틀을 만들어준다.
/* Counter.js */
import React from 'react';
export default function Counter() {
return (
<div className="counter-wrap">
<label className="title">Counter</label>
<span>0</span>
<div className="btn-wrap">
<button>-</button>
<button className="reset-btn">reset</button>
<button>+</button>
</div>
</div>
);
}
아직은 아무 동작도 하지 않는다.
Action(액션)
Action은 앱에서 발생한 일을 설명하는 이벤트라고 할 수 있는데, 상탯값 변경이 필요할 때 사용한다. type
속성값을 가지는 객체로써 생성되어야 하고 type에 해당하는 값은 고유하면서 액션이 하는 일을 설명할 수 있어야 한다.
카운터 앱에서는 카운트 증가, 감소, 리셋 세 가지 액션이 필요하므로 action.js
에 다음과 같이 액션 함수를 작성하였다. 나중에 불러와 사용해야 하므로 export로 내보낸다.
/* action.js */
export const increaseCount = () => {
return { type: 'count/increment' };
};
export const decreaseCount = () => {
return { type: 'count/decrement' };
};
export const resetCount = () => {
return { type: 'count/reset' };
};
Reducer(리듀서)
Reducer는 state
와 action
을 매개변수로 받아 필요한 경우 상태를 업데이트해서 반환하는 함수이다. 리듀서는 다음과 같은 세 가지 원칙을 따라야 한다.
💡 리듀서가 지켜야 할 규칙
- state, action을 기반으로 새 상탯값만 계산해야 한다.
- 기존 state를 수정하는 것이 아닌, 복사된 값을 변경하는 불변 업데이트를 실행해야 한다.
- 비동기 논리를 수행하거나 임의의 값을 계산하거나 다른 부작용을 일으키면 안 된다.
위 규칙을 기억하면서 화면에 표시할 카운트 숫자를 state로 지정해 리듀서를 생성한다. 이 리듀서에 액션을 전달하면 switch 문을 거쳐 각 액션에 해당하는 상탯값 업데이트를 실행하는 것이다.
/* reducer.js */
// 초기 값은 0
const initialState = {
count: 0,
};
const countReducer = (state = initialState, action) => {
const { count } = state;
switch (action.type) {
case 'count/increment': {
return {
...state,
count: count + 1,
};
}
case 'count/decrement': {
return {
...state,
count: count - 1,
};
}
case 'count/reset': {
return {
...state,
count: 0,
};
}
default: {
return {
...state,
};
}
}
};
export default countReducer;
store(스토어)
store는 상탯값을 가지고 있는 저장소라고 생각하면 된다. 리듀서를 전달함으로써 생성되어 내장 함수로 현재 state 값을 받아오거나 action을 전달해 state를 업데이트할 수 있다.
/* store.js */
import { configureStore } from '@reduxjs/toolkit';
import countReducer from './reducer';
const store = configureStore({ reducer: countReducer });
export default store;
이렇게 생성한 store
를 리액트 앱에 연동시키기 위해 index.js
파일에 react-redux 라이브러리에 내장된 <Provider>
컴포넌트를 추가하고 store를 전달해준다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import Counter from './Counter';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<Counter />
</Provider>,
);
기능 적용하기
이제 메인 컴포넌트로 돌아와 store에 저장된 상탯값을 업데이트하거나 불러와 적용하기만 하면 된다.
useSelector()
는 store의 상탯값으로부터 특정 정보를 가져올 때 사용한다. state => state.count
와 같은 형식으로 상탯값을 반환한다.
useDispatch()
는 action을 전달하여 상탯값 업데이트를 실행시키는 함수이다. 아까 action.js
파일에서 작성한 액션 함수들을 불러와 각 버튼에 적용할 이벤트 핸들러 함수를 만들어 주었다.
/* Counter.js */
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { decreaseCount, increaseCount, resetCount } from './action';
export default function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
const handleIncreaseCount = () => {
dispatch(increaseCount());
};
const handleDecreaseCount = () => {
dispatch(decreaseCount());
};
const handleResetCount = () => {
dispatch(resetCount());
};
return (
// ...
);
}
해당 함수나 값을 버튼 또는 span
내부에 전달해주면 카운터 앱이 잘 동작하는 것을 확인할 수 있다.
/* Counter.js */
export default function Counter() {
//...
return (
<div className="counter-wrap">
<label className="title">Counter</label>
<span>{count}</span>
<div className="btn-wrap">
<button onClick={handleDecreaseCount}>-</button>
<button className="reset-btn" onClick={handleResetCount}>
reset
</button>
<button onClick={handleIncreaseCount}>+</button>
</div>
</div>
);
}
References