안녕하세요! delay100입니다. 이번 포스팅에서는 Redux(리덕스)에 대해 공부해봅시다.
대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요!
책 리액트를 다루는 기술, 개정판의 16장 내용을 다루고 있습니다.
이번 포스팅의 Github 링크
https://github.com/delay-100/study-react/tree/main/ch16
1. Redux?
1-1. 리덕스란?
Redux란?
- 가장 많이 사용하는 리액트 상태 관리 라이브러리
- 컴포넌트의 상태 관련 로직을 다른 파일로 분리시켜서 더울 효율적으로 관리 가능
- 개발자 도구, 미들웨어 기능 제공해 비동기 작업을 훨씬 효율적으로 관리할 수 있게 해줌
- 컴포넌트끼리 똑같은 상태를 공유해야 할 때도 여러 컴포넌트를 거치지 않고 손쉽게 상태 값을 전달하거나 업데이트 할 수 있음
- 전역 상태를 관리할 때 효과적
1-2. 세 가지 규칙
1. 단일 스토어
하나의 애플리케이션 안에는 하나의 스토어가 들어 있습니다. 여러 개의 스토어를 사용할 수도 있지만 상태 관리가 복잡하므로 권장하지 않습니다.
2. 읽기 전용 상태
리덕스 상태는 읽기 전용입니다.
기존에 리액트에서 setState를 사용하여 state를 업데이트할 때도 객체나 배열을 업데이트하는 과정에서 불변성을 지켜 주기 위해 spread 연산자(...)를 사용하거나 immer과 같이 불변성 라이브러리를 사용했습니다.
리덕스도 마찬가지로 상태를 업데이트 할 때 새로운 객체를 생성해주어야 합니다. 리덕스에서 불변성을 유지해야하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교(shallow equality)검사를 하기 때문입니다. 리덕스는 객체의 변화를 감지할 때 객체의 깊숙한 안쪽까지 비교하는 것이 아니기 때문에 좋은 성능을 유지할 수 있습니다.
3. 리듀서는 순수한 함수
리듀서는 변화를 일으키는 함수이므로 리듀서는 순수한 함수여야 합니다. 순수한 함수는 아래의 조건을 만족합니다.
- 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받습니다.
- 파라미터 외의 값에는 의존하면 안됩니다.
- 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환합니다.
- 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 합니다.
리듀서 함수에서 사용하면 안 되는 경우 예시
등등..
- 리듀서 함수 내부에서 랜덤 값을 만드는 경우
- Date 함수를 이용해 현재 시간을 가져오는 경우
- 네트워크 요청
이에 위반하는 작업은 리듀서 함수 바깥에서 처리해줘야 합니다. 액션을 만드는 과정이나, 리덕스 미들웨어에서 처리해도 됩니다. 그리고 네트워크 요청과 같은 비동기 작업은 미들웨어를 통해 관리합니다.
2. 개념
2-1. 액션
상태에 어떤 변화가 필요하면 액션(action)이 발생합니다. 액션 객체는 type 필드를 반드시! 가져야합니다.
- type 필드: 액션의 이름
{
type: 'TOGGLE_VALUE'
}
- 그외의 필드 값들: 나중에 상태 업데이트를 할 때 참고해야 할 값(개발자 마음대로 생성 가능)
{
type: 'ADD_TODO'
data: {
id: 1,
text: '리덕스 배우기'
}
}
{
type: 'CHANGE_INPUT',
text: '안녕하세요'
}
- 액션 생성 함수: 번거롭게 매번 작성하지 않고 함수를 호출로 생성 가능
function addTodo(data) {
return {
type: 'ADD_TODO',
data
};
}
// 화살표 함수로도 생성 가능
const changeInput = text => ({
type: 'CHANGE_INPUT',
text
});
2-2. 리듀서
리듀서(Reducer)는 변화를 일으키는 함수입니다. 액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아옵니다.
받아온 현재 상태와 액션 객체 값을 참고하여 새로운 상태를 만들어 반환합니다.
- 리듀서 함수 예시
const initialState = {
counter: 1
};
// 리듀서
function reducer(state = initialState, action) { // 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아옴
switch (action.type) { // action.type에 따라 반환값이 다름
case INCREMENT:
return {
counter: state.counter + 1
};
default:
return state;
}
}
2-3. 스토어
프로젝트에 리덕스를 적용하기 위해 스토어(store)를 만듭니다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있습니다.
스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 몇 가지 중요한 내장 함수(ex. 디스패치, 구독)를 지닙니다.
2-4. 디스패치
디스패치(dispatch)는 스토어의 내장 함수 중 하나입니다. '액션을 발생시키는 것' 이라고 이해하면 됩니다.
이 함수는 dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출합니다.
이 함수가 호출되면 스토어는 리듀서 함수를 실행시켜서 새로운 상태를 만들어 줍니다.
2-5. 구독
구독(subscribe)도 스토어의 내장 함수 중 하나입니다.
subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해 주면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트 될 때마다 호출됩니다.
- 구독 함수 예시
const listener = () => {
console.log('상태가 업데이트됨');
}
const unsubscribe = store.subscribe(listener); // subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출
unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출
3. 리액트 없이 써보기
리덕스는 리액트에 종속되는 라이브러리가 아닙니다. 리액트에서 사용하려고 만들어졌지만 다른 UI 라이브러리/프레임워크와 함께 사용할 수도 있습니다.(ex. angular-redux, ember redux, Vue(는 리덕스와 유사한 vuex사용))
리덕스는 바닐라(vanilla) 자바스크립트와 함께 사용도 가능합니다. 바닐라 자바스크립트는 라이브러리나 프레임워크 없이 사용하는 순수자바스크립트 그 자체를 의미합니다.
바닐라 자바스크립트 환경에서 리덕스를 이해해보겠습니다.
3-1. 프로젝트 생성 및 실행
프로젝트를 구성하기 위해 Parcel 도구를 이용해보겠습니다. 이 도구는 쉽고 빠르게 웹 애플리케이션 프로젝트 구성이 가능합니다.
- parcel-bundler 설치 명령어
$ yarn global add parcel-bundler
# yarn global이 잘 설치되지 않는다면
$ npm install -g parcel-bundler
yarn은 실행이 잘 되지 않아서 저는 npm으로 했습니다.
- 추가 설정 명령어
# 프로젝트 디렉터리 생성
$ mkdir vanilla-redux
# 프로젝트 디렉터리로 이동
$ cd vanilla-redux
# package.json 파일 생성
$ yarn init -y
이렇게 됐으면, vanilla-redux 디렉터리 안에 package.json파일이 한 개 생기게 됩니다.
- 자동 생성된 package.json 파일 - vanilla-redux/package.json
{
"name": "vanilla-redux",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
아래는 실행 방법입니다. index.html 파일과 index.js 파일을 생성 후 실행합니다.
- Parcel 실행 방법 예시 - vanilla-redux/index.html
<html>
<body>
<div>
바닐라 자바스크립트
</div>
<script src="./index.js"></script>
</body>
</html>
- Parcel 실행 방법 예시 - vanilla-redux/index.js
console.log("hello parcel");
- parcel-bundler 실행 명령어
$ parcel index.html
실행이 완료된 후 http://localhost:1234/로 이동하면 아래의 화면이 나옵니다. 파일을 저장할 때마다 자동으로 새로고침 됩니다.
마지막으로 현재 디렉터리(vanilla-redux 안)에 리덕스를 설치합시다.
- 리덕스 설치 명령어
$ yarn add redux
3-2. UI 만들기
간단하게 UI를 구성해봅시다.
css 파일을 추가하고, html을 아래의 코드로 바꿔줍시다.
- 간단한 UI 구성하기 - vanilla-redux/index.css
.toggle {
border: 2px solid black;
width: 64px;
height: 64px;
border-radius: 32px;
box-sizing: border-box;
}
.toggle.active {
background: yellow;
}
- 간단한 UI 구성하기 - vanilla-redux/index.html
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css"/>
</head>
<body>
<div class="toggle"></div>
<hr/>
<h1>0</h1>
<button id="increase">+1</button>
<button id="decrease">-1</button>
<script src="./index.js"></script>
</body>
</html>
3-3. 리덕스 사용하기
본격적으로 리덕스를 사용해봅시다! index.js 파일을 아래의 파일로 수정하겠습니다. 모든 설명은 주석으로 달아두었습니다.
리덕스 코드를 작성하는 흐름은
- 액션 타입, 액션 생성 함수 작성
- 리듀서 작성
- 스토어 만들기
+ 스토어 구독하기(리액트에서는 react-redux 라이브러리를 사용해 스토어의 상태가 업데이트될 때마다 컴포넌트를 리렌더링)
- 리덕스 사용 예시 - vanilla-redux/index.js
import { createStore } from "redux";
// DOM 레퍼런스 만들기 - UI를 관리할 때 별도의 라이브러리를 사용하지 않기 때문에 DOM을 직접 수정해주어야 함
const divToggle = document.querySelector(".toggle");
const counter = document.querySelector("h1");
const btnIncrease = document.querySelector("#increase");
const btnDecrease = document.querySelector("#decrease");
// 액션: 프로젝트의 상태에 변화를 일으키는 것
// 액션 타입: 액션의 이름을 정의(문자열 형태로 주로 대문자로 작성, 이름은 고유해야 함)
const TOGGLE_SWITCH = "TOGGLE_SWITCH";
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";
// 액션 함수 생성: 액션 이름을 사용하여 액션 객체를 만듦
// 액션 객체는 type 값을 반드시 갖고 있어야 하며, 추후 상태 업데이트 시 참고하고 싶은 값은 마음대로 넣을 수 있음
const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });
// 초기값 설정: 초기값의 형태는 자유임(숫자, 문자, 객체)
const initialState = {
toggle: false,
counter: 0,
};
// 리듀서: 변화를 일으키는 함수
// 리듀서 함수 정의: 함수의 파라미터로는 state와 action 값을 받아옴
// 리듀서 함수가 맨 처음 호출될 때는 state값이 undefined임
// state가 undefined일 때는 initialState를 기본값으로 사용
function reducer(state = initialState, action) {
// action.type에 따라 다른 작업을 처리함
switch (action.type) {
case TOGGLE_SWITCH:
return {
// 리듀서에서는 상태의 불변성을 유지하면서 데이터에 변화를 일으켜주어야 함
...state, // spread(...) 연산자: 불변성 유지.
// 단, 객체의 구조가 복잡해지면(ex. object.something.inside.value) spread 연산자로 불변성을 관리하며 업데이트 하는 것이 번거로울 수 있고 가독성이 나빠질 수 있어서 리덕스의 상태는 최대한 깊지 않은 구조로 진행하는 것이 좋음
// 객체의 구조가 복잡해지거나 배열도 함께 다루는 경우 immer 라이브러리를 사용하면 좀 더 쉽게 리듀서 작성이 가능
toggle: !state.toggle,
};
case INCREASE:
return {
...state,
counter: state.counter + action.difference,
};
case DECREASE:
return {
...state,
counter: state.counter - 1,
};
default:
return state;
}
}
// 스토어 생성: createStore 함수 사용
// 상단에 import 구문으로 리덕스에서 해당 함수를 받아와야 함
const store = createStore(reducer); // 함수의 파라미터에 리듀서를 넣어줘야 함
// render 함수 작성: 이 함수는 상태가 업데이트될 때마다 호출됨
// 리액트의 render 함수와는 다르게 이미 html을 사용하여 만들어진 UI의 속성을 상태에 따라 변경해줌
const render = () => {
const state = store.getState(); // 현재 상태를 불러옴
// 토글 처리
if (state.toggle) {
divToggle.classList.add("active");
} else {
divToggle.classList.remove("active");
}
// 카운터 처리
counter.innerText = state.counter;
};
render();
// 구독하기: 스토어의 상태가 바뀔 때마다 방금 만든 render 함수가 호출되도록 해줌(스토어 내장함수 subscribe 사용)
// subscribe 함수의 파라미터로는 함수 형태의 값을 전달함. 전달된 함수는 추후 액션이 발생하여 상태가 업데이트 될 때마다 호출됨
store.subscribe(render);
// 디스패치: 액션을 발생시키는 것(스토어 내장함수 dispatch 사용)
// dispatch 함수의 파라미터로는 액션 객체를 넣어줌.
divToggle.onclick = () => {
store.dispatch(toggleSwitch()); // 각 DOM 요소에 클릭 이벤트 설정
// 이벤트 함수 내부에서는 dispatch 함수를 사용하여 액션을 스토어에게 전달해줌
};
btnIncrease.onclick = () => {
store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
store.dispatch(decrease());
};
오늘은 리덕스의 개념을 맛만 봐보았습니다. 앞으로 리덕스에 대해 자세히 알아보는 시간을 갖게 됩니다!!
읽어주셔서 감사합니다. 잘못된 정보는 댓글로 알려주세요!
'Study > React' 카테고리의 다른 글
17 - 추가. Redux 사용하기(1) - 앱 상태 관리 (2) | 2022.08.10 |
---|---|
17. Redux 사용하기(1) - 앱 상태 관리 (0) | 2022.08.03 |
15. Context API (0) | 2022.07.30 |
14. 뉴스 뷰어 만들기(with. newsapi) (0) | 2022.07.27 |
13. Router(라우터) & SPA(Single Page Application) (0) | 2022.07.24 |