지난 react 프로젝트에 redux 적용하기 #1에서는 리덕스가 적용되는 대략적인 구조를 만들었습니다. 그러나 앞으로 우리가 해야할 작업은 이것만으로 충분하지 않았습니다. 오늘은 비동기 작업을 위해 리덕스 청크(redux-thunk)를 이용하여 요청을 처리해보겠습니다.
리덕스 청크란 리덕스 미들웨어의 한 종류입니다. 간단한 동기 작업만이 가능한 리덕스에 비동기 작업이나, 웹 요청, 또는 저장소에 접근하는 복잡한 요청 등을 처리해주는 고마운 친구입니다.
우리는 오늘 이 고마운 친구와 함께 비동기 작업을 수행해 볼 예정입니다. 그럼 시작하겠습니다.
시작
우선 가장 문제가 되는 비동기 작업부터 처리해보겠습니다.
우리가 비동기 요청을 보냈을때의 상태를 구분해보면 세 가지가 있습니다.
1. 요청을 보내지 않았거나 아직 대기중일때
2. 요청에 성공했을때
3. 요청에 실패했을때
위 세가지 상태를 상수 형태로 선언해서 사용하겠습니다.
actions/ActionTypes.js의 코드를 아래와 같이 바꿔줍니다.
export const GET_MOVIE_PENDING = 'GET_MOVIE_PENDING';
export const GET_MOVIE_SUCCESS = 'GET_MOVIE_SUCCESS';
export const GET_MOVIE_FAILURE = 'GET_MOVIE_FAILURE';
ActionTypes.js가 바뀌었으니 index.js도 수정되어야겠죠?
actions/index.js의 코드도 수정해줍니다.
import * as types from './ActionTypes';
export function GetMoviePending() {
return { type: types.GET_MOVIE_PENDING };
}
export function GetMovieSuccess() {
return { type: types.GET_MOVIE_SUCCESS };
}
export function GetMovieFailure() {
return { type: types.GET_MOVIE_PENDING };
}
이제 리듀서에 기능을 구현해 줄 차례입니다.
reducers/movieFetch.js에 API를 받아오는 코드를 추가합니다.
function getMovieAPI() {
return fetch('https://yts.mx/api/v2/list_movies.json?sort_by=download_count')
.then((response) => response.json())
.then((json) => json.data.movies)
.catch((err) => console.log(err))
}
getMovieAPI() 를 통해 영화 데이터를 받아와 넘겨줄 수 있게 되었습니다.
아래의 movieFetch 함수도 수정해줍니다.
export const movieFetch = () => dispatch => {
console.log("movieFetch entered, dispatch start");
dispatch({type: types.GET_MOVIE_PENDING});
return getMovieAPI()
.then((response) => {
console.log("getAPIresult : ", response);
dispatch({
type: types.GET_MOVIE_SUCCESS,
payload: response
})
})
.catch((error) => {
console.log("getAPIresult : ", error);
dispatch({
type: types.GET_MOVIE_FAILURE,
payload: error
})
})
}
여기서 payload는 받아온 데이터(response)라고 생각하시면 됩니다.
중간 중간 끼어있는 console.log의 내용은 굳이 안쓰셔도 상관은 없습니다. 제가 동작 순서를 살펴보고 싶어서 적어둔거라 프로그램이 돌아가는데에는 별 다른 영향이 없습니다.
이제 state에 등록할 값들을 정해야합니다.
바뀌는 데이터들을 state에 등록하고 값을 주고 받아야하니, 필요한 값들은 현재 연결 중인지에 대한 여부(Boolean)와 에러 여부(Boolean), 그리고 불러온 영화 정보를 저장할 배열(array)정도가 있겠네요.
따라서 비어있던 initialState에 값을 다음과 같이 추가해줍시다.
const initialState = {
pending: false, // 연결 여부
error: false, // 에러 여부
movieList: [] // 영화 정보
};
초기 상태도 추가해주었으니 변경되는 값을 넣어주면 되겠습니다.
현재 타입에 따라 수행할 동작들을 핸들링 해주기 위해 관련 핸들 액션 함수를 임포트 해줍니다.
import { handleActions } from 'redux-actions';
그리고 아래 쪽에 핸들러가 수행할 내용을 입력해줍니다.
export default handleActions({
[types.GET_MOVIE_PENDING]: (state, action) => {
console.log("pending data");
return {
...state,
pending: true,
error: false
};
},
[types.GET_MOVIE_SUCCESS]: (state, action) => {
return {
...state,
pending: false,
movieList: [...action.payload]
}
},
[types.GET_MOVIE_FAILURE]: (state, action) => {
return {
...state,
pending: false,
error: true
}
}
}, initialState);
GET_MOVIE_PENDING은 pending 시기의 상태입니다. ...state로 이전의 상태를 불러오고 pending을 true로, 발생한 오류는 아직 없기 때문에 error를 false로 설정합니다.
GET_MOVIE_SUCCESS는 로드에 성공했을때의 상태입니다. 마찬가지로 ...state로 이전의 상태를 불러오고 pending은 종료되었기에 false로, movieList에 action을 통해 넘어온 데이터를 .payload를 통해 접근하여 넣어줍니다.
GET_MOVIE_FAILURE은 로드에 실패했을때의 상태입니다. ...state로 이전의 상태를, pending은 종료, error는 true, 넘어온 데이터는 없거나 잘못되었기때문에 넣어주지 않습니다.
추가로 저기서 ... 연산자가 낯설게 보이실 수도 있는데 이건 전개 구문(spread operator)라고 합니다. 배열이나 문자열처럼 반복 가능한 객체를 키-값 형식의 객체들로 확장시켜주는 역할을 하는데 간단하게 포장되어있는 객체를 한 꺼풀 벗겨낸다고 생각하시면 됩니다. 자세한 내용은 MDN 문서를 참조하세요.
우선 데이터를 받아오는 것까지는 되었으니 이제 화면에 표시해봅시다.
그러기 위해선 먼저 스토어를 살짝 수정해주어야 하는데 이참에 별도의 파일로 분리시켜주겠습니다.
src/ 경로에 store.js 파일을 생성하고 아래 코드를 입력해줍니다.
import {applyMiddleware, createStore} from "redux";
import reducers from "./reducers";
import ReduxThunk from "redux-thunk";
const store = createStore(reducers, applyMiddleware(ReduxThunk));
export default store;
간단하죠? Redux-Thunk가 적용된 store가 생성되었습니다.
그럼 원래 있던 곳의 store는 지워주시면 됩니다. src/index.js에서 원래의 store을 걷어내고, 새로 만든 store를 추가해주세요.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from "./store";
ReactDOM.render(
<React.StrictMode>
<Provider store = {store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
준비가 끝났으니 본격적으로 가져온 데이터들을 띄워보겠습니다.
App.js로 이동하여 아래의 모듈들을 임포트 해주세요.
import React, { Component } from 'react';
import './App.css';
import Movie from './component/Movie';
/* 새로 추가 */
import { connect } from 'react-redux';
import * as movieActions from './reducers/movieFetch';
import {bindActionCreators} from "redux";
기존의 App이 아닌, connect로 익스포트를 해주고, state에서 movie, loading, error 세 개의 변수를 입력해줍니다.
동시에 movieActions(movieFetch의 이름)를 디스패치 해줍니다.
export default connect(
(state) => ({
movie: state.movieFetch.movieList,
loading: state.movieFetch.pending,
error: state.movieFetch.error
}),
(dispatch) => ({
movieActions: bindActionCreators(movieActions, dispatch)
})
)(App);
componentDidMount() 부분도 아래처럼 수정해주세요.
componentDidMount() {
console.log("component mounted, fetch start");
this.props.movieActions.movieFetch();
}
이제 컴포넌트가 마운트 되었을때 자동으로 movieFetch()를 실행하게 됩니다.
_renderMovies 부분도 수정이 필요합니다. 기존의 this.state에서 데이터를 받아오지 않고, store에서 받아오는 형식으로 바뀌었기 때문에 스토어와 연결된 매개변수를 이용해서 받아주겠습니다.
_renderMovies = (movieParser) => {
const movies = movieParser.map(movieParser => {
return <Movie title={movieParser.title_english}
poster={movieParser.medium_cover_image}
key={movieParser.id}
genres={movieParser.genres}
synopsis={movieParser.synopsis}
/>
})
return movies
}
이제 스토어에 있는 데이터를 활용하는 작업까지 마쳤습니다. 그럼 이제 스토어의 값들을 꺼내 적용해봅시다.
render() 부분을 다음과 같이 수정해주세요.
render() {
const { movie, error, loading } = this.props;
console.log("renderSection ", Array.isArray(movie));
if (movie) {
console.log("로딩 완료");
}
return (
<div className={movie ? "App" : "App--loading"}>
{ loading && <h2>Loading</h2> }
{ error ? <h1>에러가 발생하였습니다</h1> : this._renderMovies(movie) }
</div>
);
}
아까 connect에서 선언해줬던 movie, error, loading을 불러와줍니다. fetch로 보낸 로딩이 완료되면 _renderMovies() 함수에 movie 상수를 인자로 호출해줍니다. 정상적으로 로딩이 성공했다면 화면이 보일 것이고, 아니라면 "에러가 발생하였습니다" 라는 메시지가 브라우저에 출력되게 됩니다.
이제 사용되지 않은 코드들을 지워주면 redux 적용이 끝납니다. ide에서 회색 또는 연한색으로 표시되기도 하고, no-use warning 등으로 콘솔에 나타나기도 하니 이 부분은 어렵지 않습니다.
남은 코드들까지 지워주면 적용이 성공적으로 끝납니다!
헤맸던 부분
처음 해보는 redux에, thunk까지 함께 하다보니 삽질에 꽤 많은 시간을 할애했습니다. 제가 했던 삽질 몇 가지가 있는데 그 중 한 가지를 기록해두려고 합니다.
이건 react나 redux의 문제는 아니고, 기본적인 javascript에 대한 무지함과 꼼꼼히 확인해보지 않은 실수가 함께 나타나서 했던 삽질입니다. redux-thunk가 적용되고, 새롭게 컴포넌트를 보여주는 코드를 짜려고 했을때 발목을 잡았던 것이 바로 어떻게 payload로 넘겨받은 데이터들을 컴포넌트에 보내줄것인가? 였습니다.
당연히 전에 사용하던 object.map을 그대로 사용하려고 했는데, 리액트 오류 메시지가 나타나며 mapping에 실패하였습니다. 에러 내용은 이렇습니다. <TypeError: movieParser.map is not a function>
그래서 리스트가 아닌가? 하는 의문을 가지고 typeof(movieParser)를 여러군데 찍어 보았으나... 어디서든 object 형식이라고만 출력되었습니다. 이때 알았던 사실이지만 아마도 typeof()는 원시타입만을 반환하는 것 같습니다. 자세한건 따로 찾아봐야겠습니다.
그래서 객체의 내용을 직접 확인해 보기로 했습니다. 처음 부분에선 의도했던 데이터는 잘 넘어왔습니다. 그러나 재가공 후 컴포넌트에 보내려고 스토어에서 꺼내오자마자 멀쩡했던 데이터들이 [object] [object] 같은 형식으로 바뀌어 접근할 수가 없었습니다.
대체 이게 무슨 일인지 한참 생각하던 끝에 동아리 선배에게 (...) 도움을 요청했고, 놓치고 있던 점이 있었음을 깨달았습니다.
movieList가 배열 [] 이 아닌 객체 {} 로 선언되어 있었습니다. 타입 에러였다니 정말 생각지도 못했었습니다. 멀쩡한 리스트 데이터를 객체에 집어넣으며 강제 형변환을 해버렸던 겁니다.
아마 강타입 언어였다면 에러 메시지가 출력되지 않았을까... 하는 생각이 들었지만 처음부터 잘 썼다면 작동했을겁니다. 알고나니 너무 허탈했지만 어쨌든 해결해서 기분은 좋습니다. 다음엔 타입부터 꼼꼼히 확인해보는 습관을 들여야겠습니다.
긴 글 읽어주셔서 감사합니다.
코드 전문은 깃허브 저장소에서 확인 가능합니다.
참고 문서
- 리덕스 미들웨어, 그리고 비동기 작업 (외부데이터 연동) by velopert
'개발 기록' 카테고리의 다른 글
A Line Translate : 한줄 번역기를 만들었어요. (크롬 확장 프로그램 개발 과정, Chrome extensions) #2 (0) | 2020.12.30 |
---|---|
A Line Translate : 한줄 번역기를 만들었어요. (크롬 확장 프로그램 개발 과정, Chrome extensions) #1 (0) | 2020.11.17 |
react 프로젝트에 redux 적용하기 #1 (0) | 2020.09.16 |
구글 클라우드 플랫폼을 이용하여 리액트 프로젝트 호스팅하기 (nginX) #3 (0) | 2020.09.12 |
구글 클라우드 플랫폼을 이용하여 리액트 프로젝트 호스팅하기 (nginX) #2 (0) | 2020.09.11 |
댓글