프로젝트 생성


Git Bash를 열고 명령어를 입력해 프로젝트를 생성
$ npx create-react-app netflix-clone

해당 디렉토리에서 필요한 모듈을 설치해 주고
$ yarn add redux react-redux axios

개발서버 실행!
$ yarn start

API 연동


최신 영화 목록, 장르, 고품질 포스터 등 다양한 정보를 가져올 수 있는 영화DB Api 사이트인 https://api.themoviedb.org 에 접속해서 회원가입 후 Setting 페이지로 이동해 Key를 발급 받자
api.JPG
위 양식을 채워 제출하면 바로 발급해서 쓸 수 있다.

Api Test

우선 사용할 수 있는 영화들을 확인하기 위해서는 장르Data를 가져와 장르목록과 id값을 알아야 하는데 아래 데이터베이스 홈페이지에서 다양한 데이터를 얻는 방법을 알 수 있다.

https://developers.themoviedb.org/3/genres/get-movie-list

image.png
장르 데이터를 얻는 방법을 확인 후 테스트 해보자

- Axios ( Genres List 얻기 )

Axios는 Promise 기반에 async/await 문법을 사용하여 XHR요청을 매우 쉽게 할 수 있어
HTTP통신을 하는데 매우 인기있는 Javascript 라이브러리이다.

useEffect(() => {
    fetchApi();
  }, []);

  const API_KEY = (your key);
  const BASE_URL = `https://api.themoviedb.org/3`;
  const Genre = `${BASE_URL}/genre/movie/list?api_key=${API_KEY}&language=en-US`;

  const fetchApi = async () => {
    const response = await axios.get(Genre);
    console.log(response.data);
  }

useEffect Hook을 사용해서 컴포넌트가 마운트 됐을 때 api를 호출되도록 코드를 작성한 후 콘솔에서 확인해보면

genres.JPG

이렇게 Genres List를 얻는데 성공했다! 사용할 장르의 아이디 값을 확인하자

Genres List

  • Action - 28
  • Comedy - 35
  • Horror - 27
  • Romance - 10749
  • Documentary - 99

`${BASE_URL}/discover/movie?api_key=${API_KEY}&with_genres=${id}`

장르를 제외한 필요한 영화정보

  • TopRated

`${BASE_URL}/movie/top_rated?api_key=${API_KEY}&language=en-US`

  • Trend

`${BASE_URL}/trending/all/week?api_key=${API_KEY}&language=en-US`

  • NetflixOriginal

`${BASE_URL}/discover/tv?api_key=${API_KEY}&with_networks=213`

Redux - Api


리덕스 언제 써야 할까?

프로젝트의 규모가 큰가?

Yes: 리덕스
No: Context API

비동기 작업을 자주 하게 되는가?

Yes: 리덕스
No: Context API

리덕스를 배워보니까 사용하는게 편한가?

Yes: 리덕스
No: Context API 또는 MobX

Context API를 사용하거나 각 컴포넌트에서 위에서 테스트한 방식으로 필요한 데이터를 가져와 사용할 수 있지만, 상태관리를 효율적으로 할 수 있는 Redux를 공부하기 위한 목적으로 만들고 있는 프로젝트기 때문에 api를 호출을 Redux에서 관리할 수 있게 만들려고 한다.

리덕스 모듈 만들기

image.png

store안에 /actions /reducers 폴더에 모듈을 분리시켜 작성한다.

action

액션은 상태에 어떤 변화가 일어날 때 발생되는데 하나의 객체로 표현되고,
type을 필수 적으로 가지고 있어야하며 그 외의 값들은 필요한대로 넣어줄 수 있다.

action type과 action 생성함수를 만들어 보자
내가 필요한 action은 각 장르들의 api를 fetch하는 것이므로 이렇게 type을 정의한다.

store /action /index.js

  • Action type
export const FETCH_TRENDING = 'FETCH_TRENDING';
export const FETCH_NETFLIX_ORIGINALS = 'FETCH_NETFLIX_ORIGINALS';
export const FETCH_TOP_RATED = 'FETCH_TOP_RATED';
export const FETCH_ACTION_MOVIES = 'FETCH_ACTION_MOVIES';
export const FETCH_COMEDY_MOVIES = 'FETCH_COMEDY_MOVIES';
export const FETCH_HORROR_MOVIES = 'FETCH_HORROR_MOVIES';
export const FETCH_ROMANCE_MOVIES = 'FETCH_ROMANCE_MOVIES';
export const FETCH_DOCUMENTARIES = 'FETCH_DOCUMENTARIES';
  • Action Creator (생성함수)

액션 생성함수는, 액션을 만드는 함수로 단순히 파라미터를 받아와서 액션 객체 형태로 만들어준다.
액션 생성함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함으로 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용한다.

export const fetchTrendData = (data) => {
  return {
      type: FETCH_TRENDING,
      data
  }
}

export const fetchTrending = () => {
  return (dispatch) => {
      return axios.get(`${BASE_URL}/trending/all/week?api_key=${API_KEY}&language=en-US`)
          .then(response => {
              dispatch(fetchTrendData(response.data))
          })
          .catch(error => {
              throw(error);
          });
  }
}

Trend 장르 Data를 받아올 수 있는 api 연동함수를 액션함수로 작성했다.
이제 리듀서 함수를 만들어야 한다.

Reducer

리듀서는 변화를 일으키는 함수다.
즉시 실행되는 익명함수로 Trend Reducer를 작성해주자
해당하는 액션타입을 불러오고 두가지의 파라미터를 받아와 현재 상태와, 전달받은 액션에 대한 새로운 상태를 만들어서 반환한다.

store /reducers /reducerTrending.js

import { FETCH_TRENDING } from '../actions/index';

export default function (state = [], action) {
  switch (action.type) {
      case FETCH_TRENDING:
          return action.data;
      default:
          return state;
  }
}

다른 장르들의 리듀서도 만들어준뒤에 combineReducers를 사용해 하나의 객체로 만들어 스토어에서 사용할 수 있게 만들어 주자

store /reducers /index.js

import { combineReducers } from 'redux';
import TrendingReducer from './reducerTrending';
import NetflixOriginalsReducer from './reducerNetflixOriginals';
import TopRatedReducer from './reducerTopRated';
import ActionMoviesReducer from './reducerActionMovies';
import ComedyMoviesReducer from './reducerComedyMovies';
import HorrorMoviesReducer from './reducerHorrorMovies';
import RomanceMoviesReducer from './reducerRomanceMovies';
import DocumentaryReducer from './reducerDocumentary';

const rootReducer = combineReducers({
  trending: TrendingReducer,
  netflixOriginals: NetflixOriginalsReducer,
  topRated: TopRatedReducer,
  action: ActionMoviesReducer,
  comedy: ComedyMoviesReducer,
  horror: HorrorMoviesReducer,
  romance: RomanceMoviesReducer,
  documentary: DocumentaryReducer,
});

export default rootReducer;

Store

리덕스에서는 한 애플리케이션당 하나의 스토어를 만들게 되고, 스토어 안에는 현재 앱의 상태와, 리듀서가 들어가있다.

src /index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import rootReducer from './store/reducers';

const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancer(applyMiddleware(thunk)));

ReactDOM.render(
    <Provider store={store}><App /></Provider>,
    document.getElementById('root')
);

serviceWorker.unregister();

Redux - thunk & devtools


redux 스토어에 루트리듀서와 thunk, 리덕스 개발자 도구를 적용했다.

Thunk

리덕스 앱에서 '사이드 이펙트'를 특별한 방법으로 처리하기 위한 Redux 미들웨어로,

만약 리듀서가 사이드 이펙트를 갖고 있으면 아래와 같은 에러가 발생하는데,
Error: Actions must be plain objects. Use custom middleware for async actions.
redux-thunk를 사용하면 이러한 문제를 해결할 수 있다.

Devtools

리덕스 개발자 도구를 사용하면 현재 스토어의 상태를 개발자 도구에서 조회 할 수 있고 지금까지 어떤 액션들이 디스패치 되었고, 액션에 따라 변화한 상태를 확인 할 수 있어 리덕스를 상태를 쉽게 알 수있고 크롬 웹 스토어 에서 설치 후 사용한다.

Hook


react-redux Hooks인 useSelector와 useDispatch를 사용해서 컴포넌트에서 리덕스가 잘 적용이 됐는지 확인해보자

해당 컴포넌트에 불러와서 사용한다.
import { useSelector, useDispatch } from 'react-redux';

useSelector

컴포넌트 내에서 리덕스 스토어 상태에 접근 할 수 있다.
const TrendData = useSelector(state => state.trending.movies, [])

useDispatch

컴포넌트 내에서 dispatch 를 사용 할 수 있다.
const dispatch = useDispatch();

src /container /TrendContainer.jsx

import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchActionMovies } from '../store/actions/index';
import Movie from '../components/Movie';

const ActionContainer = (props) => {
    const dispatch = useDispatch();
    const actionData = useSelector(state => state.action);
    const [movies, setMovies] = useState(actionData);
    console.log(actionData);

    useEffect(() => {
        dispatch(fetchActionMovies());
    }, []);

    return (
        <div>
             { movies.map(movie => (
                <Movie img={movie.poster_path} key={movie.id}/>
            ))}
        </div>
    )
}

export default ActionContainer;

이제 랜더링 해보자

image.png

개발자 도구에서 확인해 보니 데이터가 잘 들어와있는 상태였지만
TypeError: Cannot read property 'map' of undefined 에러가 발생했고,

액션은 잘 발생했지만 어째서인지 actionData는 비어있는 값으로 나타났다 ㅠㅠ

Stack Overflow


비전공자인 내가 혼자 공부를 하고있다 보니 주변에 물어볼 사람이 없기 때문에 공부를 하다 막히면 해결하는데 시간을 많이 소비하게 되는데,
처음 기초 공부를 할 때는 카카오 오픈채팅방이나 개발관련 네이버카페에 질문을 올렸었다.

스택 오버플로우를 알게 된 후로는 여기서 많은 도움을 받고있는데,
질문을 올리기 전에는 스스로 다양한 방법을 찾아 시도해 보고 안될 때
질문을 하는게 좋고 영어를 사용해야 한다.
대부분은 이미 답이 나와있는 경우가 많지만 하루정도 삽질을 하다가 혼자서는 도저히 해결이 안날 것 같아 스택 오버플로우에 질문을 남겨보기로한다.

Stack Overflow

개발자들이 프로그래밍을 하다 막혔을 때, 또는 프로그래밍에 대한 질문을 하고 답변을 받는 사이트이다. 웬만한 엘리트 굇수 프로그래머들은 이 포인트/배지 수가 어마어마하다. 규모로는 웬만한 개발자 커뮤니티 중에선 가장 크다고 봐도 무난하다. 답변이 매우 빨리 올라오
기 때문에 급한 질문은 여기서 묻는 게 좋다.
하지만 규모가 크기 때문에, 자신이 헤매고 있는 질문에 대한 해답은 이미 올라와 있는 경우가 대부분이다. 즉, 질문을 올리기보다는 그냥 검색을 해서 답변을 얻는 경우가 더 많다는 것.
디버깅하다가 어떠한 에러/문제에 대해 구글링을 하면 가장 먼저 나오는 웹사이트이기도 하다.

질문에 대한 답이 와서 확인해 보니 나는 절대 변경되서는 안되는 리듀서의 state를 변경(mutate)하고있었다고 한다. 그래서 뷰에 변화가 일어나지 않았고 더 검색을 해보니 답을 알 수 있었다.

액션을 디스패치했는데 아무 일도 일어나지 않는 이유

오류해결


Reducer 수정

store /reducers /reducerTrending.js

import { FETCH_TRENDING } from '../actions/index';

export default function (state = [], action) {
  switch (action.type) {
      case FETCH_TRENDING:
          return { // 변경된 부분
              ...state,
            movies: action.data
          }
      default:
          return state;
  }
}

이렇게 state를 변경하지 않고 새로운 상태의 객체를 반환시켜야 Redux를 예측 가능하고 능률적으로 만들어준다.

Container 수정

src /container /TrendContainer.jsx

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchTrending } from '../store/actions/index';
import Movie from '../components/Movie';

const TrendContainer = (props) => {

        const dispatch = useDispatch();

        useEffect(() => {
            dispatch(fetchTrending());
        }, []);

        const TrendData = useSelector(state => state.trending.movies, []) || [];
        // 변경된 부분 you could use memoization to improve performance.

    return (
        <div>

            <p>Trend Movies</p>
            <div className="movieContainer"> // 변경된 부분
                { TrendData.results && TrendData.results.map(movie => (
                    <Movie img={movie.backdrop_path} key={movie.id}/>
                ))}
            </div>

        </div>
    )
}

export default TrendContainer;

답변을 주신 분께서 더 효율적으로 코드를 작성할 수 있는 방법을 알려주셨다.

Memoization

메모이제이션이란 프로그래밍을 할 때 반복되는 결과를 메모리에 저장해서 다음에 같은 결과가 나올 때 빨리 실행하는 코딩 기법을 말합니다.

반복되는 작업이 많은 코드에 경우 메모제이션패턴을 사용하면 더 빠르고 효율적으로 결과를 추출할 수 있다는 것이 장점이다.

이렇게 해서 컴포넌트를 리렌더링 해보면 poster가 잘 가져와진다.
image.png

Redux에서 api연동을 관리하고 컴포넌트에서 해당 데이터를 UI로 뿌리는데 작업을 성공했다!

다음 포스트에서는 컴포넌트구조와 레이아웃을 구성해보자.

참조


https://deminoth.github.io/redux/advanced/AsyncActions.html
https://github.com/reduxjs/redux-thunk
https://react.vlpt.us/redux-middleware/05-redux-thunk-with-promise.html
https://react-redux.js.org/next/api/hooks