GDSC FE-Toy Project Study Plan
- 'React를 다루는 기술' Ch17, 18 공부 및 실습 진행
[Week 10] 1/27
- 발표자
- Ch17. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 - 하수민
- Ch18. 리덕스 미들웨어를 통한 비동기 작업 관리 - 장효신
Ch18. 리덕스 미들웨어를 통한 비동기 작업 관리
1) 미들웨어란?
리덕스 미들웨어란?
리덕스에서 액션을 디스패치했을 때, 리듀서에서 이를 처리하기에 앞서
사전에 지정된 작업들을 실행함
미들웨어는 액션과 리듀서 사이의 중간자
👉 액션을 디스패치하고나서 어떤 작업들을 연쇄적으로 실행해야 할 때 사용할 수 있음
미들웨어가 할 수 있는 일
- API 요청에 대한 상태를 관리
- 예를 들어 요청이 시작됐을 때는 로딩 중임을,
- 요청이 성공하거나 실패했을 때는 로딩이 끝났음을 명시해야 함
- 전달받은 액션을 콘솔에 기록하거나
- 전달받은 액션을 기반으로 액션을 취소하거나
- 다른 종류의 액션을 추가로 디스패치할 수도 있음
2) 실습
실습을 통해서 미들웨어를 사용한 비동기 작업을 관리해보겠다.
다음 순서로 진행한다.
- 작업 환경 준비
- 미들웨어 직접 만들기
- redux-logger 사용하기
- 미들웨어를 사용한 비동기 작업 관리
작업 환경 준비
1. CRA(create react app)을 사용해서 리엑트 프로젝트를 생성한다.
2. 리덕스를 사용해서 카운터를 구현할 것이므로
이에 필요한 라이브러리인 `redux`와 `react-redux`와 `redux-actions`을 설치한다.
cf. 각 라이브러리에 대한 설명
- redux:
- react-redux:
- redux-actions:
다음 명령어를 사용하면 된다.
$ npm add redux react-redux redux-actions
3. 리덕스를 위한 코드를 준비한다.
- 먼저, counter 리덕스 모듈을 작성한다.
카운터 작동 확인
미들웨어 만들기
- 실제 프로젝트에서는 다른 개발자가 만들어 놓은 미들웨어를 사용하므로 미들웨어를 직접 만들어서 사용하는 일이 많지 않음
- 하지만 여기에서는 미들웨어의 작동방식을 제대로 이해하기위해 직접 만들어봄
- 원하는 미들웨어가 없을 때 직접 만들거나 기존 미들웨어를 커스터마이징해서 사용해볼 수 있음
액션이 디스패치될 때마다 액션 정보와 액션 디스패치되기 전후의 상태를 콘솔에 보여주는
로깅 미들웨어를 작성해보자
리덕스 미들웨어 구조
미들웨어는 결국 함수를 반환하는 함수
함수 파라미터에는 store, next, action 3가지가 있음
- store: 리덕스 스토어 인스턴스
- next: 함수 형태이며 store.dispatch와 비슷한 역할을 함
- action: 디스패치된 액션
const loggerMiddleware = store => next => action => {
// 미들웨어 기본 구조
}
export default loggerMiddleware;
- 화살표 함수를 연달아서 사용함
일반 function 키워드로 풀어서 쓴다면 다음 구조임
const loggerMiddleware = function loggerMiddleware(store){
return function(next){
return function (action){
// 미들웨어 기본 구조
};
};
};
loggerMiddleware 작성
const loggerMiddleware = store => next => action => {
// 액션 타입으로 log를 그룹화함
console.group(action && action.type);
// 1. 스토어 상태 조회
console.log('이전 상태', store.getState());
console.log('액션', action);
// 2. 다음 미들웨어나 리듀서에게 액션을 전달함
next(action);
// 업데이트된 상태
console.log('다음 상태', store.getState());
// 그룹 끝
};
console.groupEnd();
export default loggerMiddleware;
다음, 리덕스 미들웨어를 스토어에 적용한다.
실행 결과
redux-logger 사용하기
- 이번엔 오픈소스 미들웨어인 redux-logger를 사용해보자
- 앞서 만든 loggerMiddleware보다 훨씬 더 잘 만들어진 라이브러리이며,
- 브라우저 콘솔에 나타는 형식도 훨씬 깔끔하다.
다음 명령어로 설치함
$ npm add redux-logger
index.js 수정
- 액션 디스패치 시간도 나타남
- 콘솔에 색상이 입혀짐
redux-logger
비동기 작업을 처리하는 미들웨어 사용
오픈 소스 미들웨어를 사용해서 비동기 작업을 효율적으로 관리해보겠다.
여기에서는 redux-thunk와 redux-saga를 다룸
- redux-thunk: 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어로
- 객체가 아닌 함수 형태의 액션을 디스패치 할 수 있음
- redux-saga : redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 미들웨어 라이브러리로,
- 특정 액션이 디스패치되었을 때 정해진 로직에 따라 다른 액션을 대스패치하는 규칙을 작성해서 비동기 작업을 처리함
redux-thunk
리덕스의 창시자인 댄 아브라모프가 만듦
리덕스 공식 메뉴얼에서도 이 미들웨어를 사용해서 비동기 작업 다룸
Thunk란?
- 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것
예를 들어, 파라미터에 1을 더하는 함수를 만들고 싶다면 다음과 같이 작성할 수 있고
addOne 호출하면 바로 1 + 1이 연산됨
const addOne = x => x + 1;
addOne(1); // 2
그런데 이 작업을 나중에 하도록 미루고 싶다면?
const addOne = x => x + 1;
const addOneThunk = x => () => addOne(x);
const fn = addOneThunk(1);
setTimeout(()=>{
const value = fn(); // fn이 실행되는 시점에 연산
console.log(value);
}, 1000);
redux-thunk는 다음 명령어로 라이브러리 설치
$ yarn add redux-thunk
index.js에서 store를 만들 때 적용한다.
import ReduxThunk from 'redux-thunk';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
Thunk 생성 함수 만들기
increaseAsync와 decreaseAsync를 만들어서 카운터값을 비동기적으로 변경시켜보자
- redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환함
modules/counter.js 수정
// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
setTimeout(()=>{
dispatch(increase());
}, 1000); // ms 단위
};
export const decreaseAsync = () => dispatch => {
setTimeout(()=>{
dispatch(decrease());
}, 1000); // ms 단위
}
const initialState = 0; // 상태는 꼭 객체일 필요가 없음
const counter = handleActions(
{
[INCREASE]: state => state + 1,
[DECREASE]: state => state-1
},
initialState
);
export default counter;
CounterContainer 수정
increase => increaseAsync
decrease => decreaseAsync로 수정
import { increaseAsync, decreaseAsync } from '../modules/counter';
import Counter from '../components/Counter';
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<Counter number={number} onIncrease={increaseAsync} onDecrease={decreaseAsync} />
);
};
export default connect(
state => ({
number: state.counter
}),
{
increaseAsync,
decreaseAsync
}
)(CounterContainer);
실행화면
- redux-thunk는 처음 디스패치되는 액션은 함수 형태이고, 두 번째 액션은 객체 형태임
웹 요청 비동기 작업 처리하기
thunk의 속성을 활용하여 웹 요청 비동기 작업을 처리하는 방법에 대해 알아보겠다.
JSONPlaceholder에서 제공되는 가짜 API를 사용할 것
사용할 API
# 포스트 읽기(:id는 1~100 사이 숫자)
GET https://jsonplaceholder.typicode.com/posts/:id
API를 호출할 때는 Promise 기반인 웹 클라이언트인 axios를 사용함
$ yarn add axios
lib/api.js
API를 함수화한다.
- API를 호출하는 함수를 따로 작성하면 가독성이 좋고 유지보수도 쉬워짐
- export를 사용해서 내보냄
import axios from 'axios';
export const getPost = id =>
axios.get(https://jsonplaceholder.typicode.com/posts/</span><span class="co49">${</span><span class="cd2 co33">id</span><span class="co33">}</span><span class="cd2 co31">);
export const getUsers = id =>
axios.get(https://jsonplaceholder.typicode.com/users);
리듀서 작성
import {handleActions} from 'redux-actions';
import * as api from '.../lib/api';
// 액션 타입 선언
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_POST_FAILURE = 'sample/GET_POST_FAILURE';
const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'smaple/GET_USERS_SUCCESS';
const GET_USER_FAILURE = 'sample/GET_USERS_FAILURE';
// thunk 함수 생성
export const getPost = id => async dispatch => {
dispatch({type: GET_POST}); // 요청 시작 알림
try{
const response = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: response.data
}); // 요청 성공
} catch(e){
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw(e) // 나중에 컴포넌트에서 에러를 조회할 수 있게 해줌
}
};
};
// 초기 상태 선언
const initialState = {
// 요청의 로딩 중 상태 관리
loading: {
GET_POST: false,
GET_USERS: false
},
post: null,
users: null
};
const sample = handleActions({
[GET_POST]: state => ({
...state,
loading: {
...state.loading,
GET_POST:true // 요청 시작
}
}),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST:false // 요청 완료
},
post: action.payload
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST:false // 요청 완료
},
}),
initialState
);
export default sample;
index.js에 루트 리듀서에 포함시키기
import {combineReducers} from 'redux';
import counter from './counter';
import sample from './sample';
const rootReducer = combineReducers({
counter,
sample
});
export default rootReducer;
다음은, 위에서 불러온 데이터를 렌더링해 줄 프레젠테이셔널 컴포넌트 작성
이때, 유효성 검사를 꼭 해야한다
- 조건부 렌더링 연산자인 &&를 작성하면
- post가 유효할 때만 값을 보여줄 수 있음
- 데이터가 없는 상태라면 post.title을 조회할 때 오류가 발생하니 꼭 유효성 검사를 해줘야 함
(...)
<section>
<h1>포스트</h1>
{loadingPost && '로딩 중...'}
{!loadingPost && post && (
<div>
<h3>{post.title}</h3>
<h3>{post.body}</h3>
</div>
)}
</section>
<hr />
<section>
<h1>사용자 목록</h1>
{loadingUsers && '로딩 중...'}
{!loadingUsers && users && (
<ul>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.email})
</li>
))}
</ul>
)}
</section>
(...)
sample container 작성
- useEffect로 데이터 로딩
- 뎁스에 getPost와 getUsers를 줌
- connect로 스토어에서 상태 조회한거랑 리듀서 내려보내 줌
import React, { useEffect } from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample";
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
useEffect(() => {
getPost(1);
getUsers(1);
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
export default connect(
({ sample }) => ({
post: sample.post,
users: sample.users,
loadingPost: sample.loading.GET_POST,
loadingUsers: sample.loading.GET_USERS
}),
{
getPost,
getUsers
}
)(SampleContainer);
실행 결과
'3-1기 스터디 > Front-End 토이 프로젝트' 카테고리의 다른 글
[8주차] 외부 API 연동 news viewer 제작 (0) | 2022.01.30 |
---|---|
[9주차] Context API, 리덕스 라이브러리 이해하기 (0) | 2022.01.30 |
[7주차] immer를 사용해 쉽게 불변성 유지하기 (0) | 2022.01.07 |
[6주차] 컴포넌트 반복, 라이프사이클 메서드, 함수형 컴포넌트 (0) | 2021.12.22 |
[5주차] 이벤트 핸들링, ref: DOM에 이름 달기 (0) | 2021.11.25 |
댓글