# Movie API

다음 링크에는 (opens new window) 각종 영화들에 대한 정보에 접근할 수 있게 API DOCS가 작성되어 있습니다.

https://yts.torrentbay.to/api/v2/list_movies.json에 GET요청을 보내면 되는데, 파라미터로 minimum_rating을 전달하면 최저 평점 이상의 영화 정보들만 모아서 가져올 수 있습니다. 기본적인 네트워크 요청 방식에 대해 알고싶으시면 AJAX TODO 문서의 Axios 투두리스트 만들기 (opens new window) 문서를 참조해주세요.

데이터 요청 후 받아온 영화 정보들을 렌더링해줍니다.

import { useState, useEffect } from 'react';
import Movie from './Movie';

function App() {
    const [loading, setLoading] = useState(true);
    const [movies, setMovies] = useState([]);
    const getMovies = async () => {
        const response = await (
            await fetch(
                'https://yts.mx/api/v2/list_movies.json?minimum_rating=9&sort_by=year'
            )
        ).json();
        setMovies(response.data.movies);
        setLoading(false);
    };

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

    return (
        <div>
            {loading ? (
                <h1>Loading...</h1>
            ) : (
                <div>
                    {movies.map((movie) => (
                        <div key={movie.id}>
                            <img src={movie.medium_cover_image} alt='' />
                            <h2>{movie.title}</h2>
                            <p>{movie.summary}</p>
                            <ul>
                                {movie.genres.map((g) => {
                                    return <li key={g}>{g}</li>;
                                })}
                            </ul>
                        </div>
                    ))}
                </div>
            )}
        </div>
    );
}
export default App;

리액트 라우팅 기능 이용에 앞서 기본적인 애셋을 제작하였습니다.

# Routing

본격적으로 리액트 라우팅을 알아보게 될텐데, 우선 필요성에 대해 알아야 할 것 같습니다.

당장 위의 영화 정보 서비스만 하더라도 각 영화에 대한 디테일한 정보가 담겨져있을텐데 이를 한 페이지에서 모두 표기하기에는 너무나 많은 리소스를 요청하게 됩니다. 따라서 메인 페이지에서는 최소한의 것들, 즉 영화 데이터만 제공을 한 상황에서 특정 영화를 클릭했을때 해당 영화 정보를 디테일하게 보여주는 로직의 구현이 필요합니다.

이때 바로 리액트의 라우팅 기법이 사용되는 것입니다. 스크린 단위로 라우터를 설계한 후 진행합니다.

먼저 리액트 프로젝트가 CRA 기반이라는 것을 가정하고, react-router-dom을 설치해줍니다. (현 문서 작성 시점에서 버전은 6.3.0입니다.)

npm install react-router-dom

리액트 프로젝트를 CRA로 생성했으면 주로 작업하게될 src 폴더가 있을겁니다. 해당 폴더 아래에 routes 폴더를 생성하고 App.js에서 작업했던 홈스크린 렌더링 코드들을 모두 이동시켜줍니다. (앞으로 화면을 그리는 작업들은 App.js에서 하지 않게 됩니다.)

리액트 라우터 6.X버전의 경우 SwitchRoutes로 변경되었습니다. 또한, 컴포넌트를 표기해주는 라우터 프로퍼티 이름이 component에서 element로 바뀌었습니다. 기존의 홈 스크린 라우팅 코드와 6.X 버전 코드를 비교하면 다음과 같습니다.

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Home from './routes/Home';

function App() {
    return (
        <Router>
            <Switch>
                <Route path='/' component={<Home />} />
            </Switch>
        </Router>
    );
}
// 6.X 버전
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './routes/Home';

function App() {
    return (
        <Router>
            <Routes>
                <Route path='/' element={<Home />} />
            </Routes>
        </Router>
    );
}

사용된 라우팅 컴포넌트 몇 가지 역할에 대해 정리하면 다음과 같습니다.

  1. BrowseRouter - 라우트 path에 따라 UI를 동기화 시켜주는 컴포넌트입니다. (라이브러리 자체적으로 HTML5 API를 활용합니다.) 쉽게 말해, 입력되는 URL에 따라 UI 렌더링 로직을 처리해주는 최상위 컴포넌트라고 보시면 됩니다.
  2. Routes - BrowseRouter 내에서는 여러 Route 컴포넌트들을 동시에 렌더링할 수 있지만, Routes에 감싸진 라우트 컴포넌트들은 URL에 따라 하나씩만 렌더링되게 됩니다.
  3. Route - path와 일치하는 URL에서 전달된 컴포넌트 UI를 렌더링합니다.

Routes로 감싸지지 않은 Route에서 URL파라미터 활용을 위해 pathpath="/:users" 라고 정의한다면, 실제 URL 구성에서 https://...../:users라는 URL이 없다면 404페이지를 띄우게 되는 겁니다.

여기까지 보면 리액트 라우터를 굳이 사용해나 싶기는 합니다. 컴포넌트를 path에 등록된 URL에 따라 렌더링 한다는 것이죠. HTML에서 anchorhref 속성을 통해 라우트 설정된 컴포넌트 링크로 이동하면 되는 것 아닌가요?

이때 SPA의 위력이 발휘됩니다. HTML anchor태그를 통해 설정된 라우터에 접근이 분명 가능합니다. 하지만 페이지 전체가 리로딩된다는 단점이 있는 것이죠. 이에 따라 SPA기반의 라우트 컴포넌트 설정이 사실상 의미가 없게 되는 것입니다.

리액트 라우터는 페이지 전체가 리로딩되는 것처럼 보이게 하지만, 사실은 한 페이지 내에서 컴포넌트의 교체만 동적으로 이루어지게 되는 것입니다. 이때 사용되는 컴포넌트가 바로 react-router-dom<Link></Link> 컴포넌트입니다. Link 컴포넌트의 to 프로퍼티를 사용하면 이동하고 싶은 링크를 설정할 수 있습니다.

import { Link } from 'react-router-dom';

function Movie({ title }) {
    return (
        <div>
            <h2>
                <Link to='/movie'>{title}</Link>
            </h2>
        </div>
    );
}

Link에 styled-components로 스타일링하기

Link태그에 styled-components를 적용하여 스타일링할 수 있습니다. CSS 프로퍼티인 text-decoration:none과 같은 스타일들을 인라인 형태로 처리해도 되지만, 좀 더 많은 스타일링을 하고자 한다면 styled-components의 상속을 활용합니다.

import styled from 'styled-components';

const MyLink = styled(Link)`
    text-decoration: none;
    &:hover {
        background-color: black;
    }
`;

function Component() {
    return <MyLink to='/' />;
}

Link컴포넌트를 다른 컴포넌트에서 상속 받아 스타일링을 진행했으면 Link 컴포넌트에서 필수적으로 요구하는 프로퍼티들에 대해 값을 각각 설정해야합니다. 예를 들어 Linkto라는 프로퍼티 값을 반드시 설정해줘야 하므로 MyLink로 커스텀 컴포넌트 스타일링을 진행했더라도 위 코드처럼to 값을 부여해야 합니다.

# useNavigate

react-router-dom에서는 HTML5 history를 기반으로 뒤로가기 기능을 제공해줍니다. v5에서는 useHistory라는 훅 네이밍을 갖고 있었지만 v6로 업데이트 되면서 useNavigate라는 훅 네이밍으로 바뀌었습니다.

useNavigate를 사용하려면 당연히 Routes (v5에서 Switch컴포넌트)를 통한 페이지 이동 컴포넌트 구조가 구성되어 있어야 합니다. URL을 /, /child로 구성했다고 할때 다음 코드를 보면,

// Home URL '/'
// Route 구성된 상태
import { useNavigate } from 'react-roouter-dom';

function Home() {
    const navigate = useNavigate();
    return <Link onClick={() => navigate('/child')}>Go To Child!</Link>;
}

export default Home;
// Child URL "/child"
import { useNavigate } from 'react-router-dom';

function Child() {
    return <Link onClick={() => navigate(-1)}> Go Back! </Link>;
}

Home에서 Go To Child! 버튼을 클릭하면 /child로 이동하게 되고, Go Back!버튼을 클릭하게 되면 HTML History API 기준 이전의 페이지로 이동하게 됩니다. 위 코드에서는 /으로 이동하게 됩니다.

navigate()함수에 -1과 같이 음수값을 전달하면 현재 페이지 기준으로 접속했던 과거 N번째 페이지로 이동하게 되는 것입니다.

# URL parameter

URL의 구성 요소에는 정적인 파라미터만 있는 것이 아닙니다. 라우트 컴포넌트를 통해 URL 구성이 모듈화되어 있다고 해도, 그 동일한 모듈이 재사용되는 서비스의 경우 고유한 값에 따라 페이지 URL을 다르게 구성해야 하는 것이죠.

쉽게 말해 우리가 컴포넌트 생성 시 map() 메서드로 여러 HTML 엘리먼트들을 생성하였고, 그 엘리먼트를 클릭했을때 이동하는 Link 컴포넌트를 생성하는 상황에서 몇 개가 될지 모르는 HTML 엘리먼트에 대해 각각 이름을 다르게 붙여줄 수 있을까요?

이럴 때에 URL 동적 파라미터를 사용하게 되는 것입니다. Link 컴포넌트의 path에 상위 컴포넌트로부터 넘어온 고유값 프롭스를 to 프로퍼티에 전달하면 됩니다.

<Link to={`/movie/${id}`}>{title}</Link>

링크 컴포넌트 to 프로퍼티에 콧수염 괄호를 열어주고, 상위 컴포넌트로부터 받아온 프롭스 id를 전달합니다. 상위 컴포넌트는 현재 map을 통해 Movie 컴포넌트를 여러개 생성해주고 있죠.

{
    movies.map((movie) => (
        <Movie
            key={movie.id}
            id={movie.id}
            medium_cover_image={movie.medium_cover_image}
            genres={movie.genres}
            title={movie.title}
            summary={movie.summary}
        />
    ));
}

Link 컴포넌트를 활용하여 여러 개의 컴포넌트들을 무사히 렌더링할 수 있었습니다. 그렇다면 이번엔 App.js, 즉 전체 라우트 컴포넌트들을 관리하는 최상위 컴포넌트로 이동해봅시다.

function App() {
    return (
        <Router>
            <Routes>
                <Route path='/movie' element={<Detail />} />
                <Route path='/' element={<Home />} />
            </Routes>
        </Router>
    );
}

현재 Route 컴포넌트 중 영화 디테일 페이지를 렌더링하는 path/movie 라고 되어있습니다. 이곳에 동적 파라미터를 등록하고 싶다면 URL파라미터 이름 앞에 콜론만 붙여주면 됩니다.

<Route path='/movie/:myid' element={<Detail />} />

이후 만약 디테일 컴포넌트 내에서 myid라는 URL파라미터와 또는 그 외의 파라미터 값들을 모두 알아야하는 상황이 있습니다. YTS로부터 영화 디테일 정보들을 받아온다고 했을때 URL 파라미터에 전달된 myid값을 가지고 이에 해당하는 영화를 찾아 디테일 정보를 요청한다고 가정해봅시다. 이러한 상황에서 사용되는 리액트 훅이 바로 useParams 입니다.

import { useParams } from 'react-router-dom';

function Detail() {
    const x = useParams();
    console.log(x);
    return <h1>Detail</h1>;
}

export default Detail;

Detail 컴포넌트를 렌더링하면 x에 URL 파라미터 값들이 객체 형태로 전달됩니다.

x = { myid: '31234' };

현재까지의 흐름을 정리하면 다음과 같습니다.

  1. react-router-dom의 Link 컴포넌트를 통해 HTML href 대신 컴포넌트 렌더링 로직으로 대체한다. 정적 리소스 재요청을 막기 위함입니다. 이때 to 프로퍼티에는 to={프롭스값}과 같은 형태, 즉 동적인 값을 전달해야겠죠.
  2. Route 컴포넌트에서 path에 콜론을 붙인 URL파라미터 이름을 전달합니다. <Route path="/movie/:id" element={<Detail/}과 같은 형태입니다.
  3. 라우트 컴포넌트를 통해 새롭게 렌더링된 Detail 컴포넌트에서 다양한 로직 처리를 위해 URL 파라미터의 값들을 받아와야 합니다. 이때 useParams 훅을 사용합니다. URL파라미터 전체 값이 객체 형태로 반환됩니다.

다음은 디테일 컴포넌트에서 URL파라미터 값을 추출하여 API요청을 보내는 코드입니다.

function Detail() {
    const { id } = useParams(); // Params 추출

    const getMovie = async () => {
        const json = await // Params로 API요청
        (
            await fetch(
                `https://yts.mx/api/v2/movie_details.json?movie_id=${id}`
            )
        ).json();
        console.log(json);
    };
    useEffect(() => {
        getMovie();
    }, []);
    return <h1>Detail</h1>;
}

Typescript useParams

React Router Dom 사용시 v6 이상이면 useParams 훅 사용시 타입을 특정짓지 않아도 됩니다. useParams훅을 사용한 이상 타입이 string 또는 undefined로 지정됩니다.

# useLocation

react-router-dom의 훅 중에서는 useLocation이 있다. 자바스크립트에서의 location 객체가 간소화된 형태로 제공된다고 보면 됩니다.
useLocation훅이 location 객체를 다룬다는 것 자체에도 의미가 있지만 이전 페이지 라우터 컴포넌트로부터 프롭스를 받아올 수 있다는 점에서 유용하게 사용되고는 합니다.

코드를 보자.

import { Link } from 'react-router-dom';

// ....
<Link to={`/${myfile.id}`} state={myfile.name}>
    <Img src={`${endpoint}/myFile.txt}`} alt='coin' />
    {coin.name} &rarr;
</Link>;

Link 컴포넌트를 통해 특정 API에 요청을 보냅니다. 이때 이 컴포넌트에는 state 프로퍼티를 전달할 수 있습니다.

중요한건 프롭스로 state를 받는 컴포넌트에서 인터페이스를 정의해야한다는 것입니다. 위의 코드에서 만약 myFile.namestring타입이었다면 하위 컴포넌트에서의 인터페이스는 다음과 같이 정의되어야 할 것입니다.

interface FileState{
  state: string;
}

function Child(){
  const { state } = useLocation() as FileState;
}

useLocation으로부터 구조 분해 되어 state에는 상위 컴포넌트의 프롭스로부터 전달받은 myFile.name 문자열 값이 저장되게 됩니다.

WARNING

위 코드의 하위 컴포넌트가 state을 사용하기 위해서는 상위 컴포넌트의 프롭스로부터 state 값을 전달받아야 합니다.
Link를 통해 순서가 있는 접근이 아닌 하위 컴포넌트 URL로 직접 접근하게 되면 undefined 값이 저장되게 됩니다. 이는 분명 논리적 오류이며 이를 예외적으로 처리해야합니다.

널 병합 연산자 ??를 통해 처리하도록 해봅시다.

function Child(){
  const { state } = useLocation() as FileState;

  return (
    <div>
      <span>{state ?? "UNDEFINED!!"}</span>
    </div>
  )
}

state가 객체 형태로 프롭스에 전달되었다면 옵셔널 체이닝으로 깔끔한 코딩을 할 수도 있습니다. state 객체의 name 프로퍼티가 있다고 가정해봅시다.

function Child(){
  const { state } = useLocation() as FileState;

  return (
    <div>
      <span>{state?.name ?? "UNDEFINED!!"}</span>
    </div>
  )
}

state객체가 undefined가 아니면 해당 객체의 name 프로퍼티 값을 출력하고 이 또한 undefined면 후에 작성된 UNDEFINED!!를 부착합니다.

# Nested Router

라우터가 중첩된 형태를 가지면 Nested Router이다. 유용하게 사용될텐데, 바로 탭 기능 구현에 사용된다.

react-router-dom의 Outelet 컴포넌트를 사용하면 Router.tsx에 정의된 중첩 라우트가 자동으로 삽입된다. 컴포넌트의 구조가 Todos가 있고 완료 여부를 route path에 넘기는 형식이라고 가정하자.

이때 Todo 컴포넌트 안에 중첩 라우트를 사용하는 상황이다.

// Router.tsx
function Router() {
    return (
        //...
        <Routes>
            <Route path='/todos' element={<Todos />}>
                <Route path='completed' element={<Completed />} />
                <Route path='incompleted' element={<Incompleted />} />
            </Route>
        </Routes>
    );
}

위와 같이 한 투두 리스트 안에 완료된 투두 / 완료되지 않은 투두를 고를 수 있도록 중첩 라우트를 구성한 상태에서 URL "/todos/completed"에 접근할 때 중첩 라우트를 분리하여 코드 작성할 필요없이 Outlet 컴포넌트를 사용하면 URL 검사와 함께 부모 / 자식 컴포넌트를 비교하여 자동으로 위 구조 컴포넌트를 렌더링해준다.

// In Router.tsx

<Route path="/:todoId/*" element={<Todo/>} />

// In Coin.tsx

<Routes>
  <Route path="completed" element={<Completed />} />
  <Route path="incompleted" element={<Incompleted />} />
</Routes>

위 처럼 분리하는 것이 아니라,

// In Router.tsx
<Route path='/:coinId' element={<Coin />}>
    <Route path='chart' element={<Chart />} />
    <Route path='price' element={<Price />} />
</Route>

위 처럼 라우터 파일에 중첩 라우트를 구성하고 위의 Coin/Chart&Price 구조의 컴포넌트를 부착할 위치에 Outlet 컴포넌트를 부착하면 된다는 것이다.

예시 코드를 살펴보자. 중첩 라우트 구성이 위처럼 /:coinId/chart, /:coinId/price라고 할 때 Outlet 컴포넌트와 함께 context로 URL을 전달하면 된다.

function Component() {
    const selectURL = true; // true or false.. 우선은 하드코딩
    return <Outlet context={{ url: select ? chart : price }} />;
}

context는 객체 형태로 전달해야한다.

Outlet에 전달된 context의 select 프로퍼티값이 true / false냐에 따라 chart / price로 Outlet이 렌더링 되는 컴포넌트가 달라지게 된다.

Outlet 대상인 중첩 라우트 하위 컴포넌트에서는 useOutletContext훅을 통해 상위 컴포넌트의 context프로퍼티 값을 받아올 수 있습니다. 타입스크립트에서는 해당 훅에 제네릭까지 선언해야합니다.

interface IContext{
  myContext: string;
}

function Component(){
  const {myContext} = useOutletContext<IContext>();
}

# Outlet Context

리액트 쿼리에서 우리는 중첩 라우트를 구현했었다. 버전6으로 업데이트 되면서 Outlet 컴포넌트를 활용할 수 있게 되어 이를 활용하였었는데 중첩 라우트의 자식 컴포넌트에 프롭스를 전달하기 위해서는 어떻게 해야할까?

컨텍스트 개념이 등장하여 Outlet 컴포넌트에 전달할 수 있게 되었다. 나중에 Outlet으로 대체될 중첩 라우트에 특정 프롭스가 정의되어 있지 않더라도 Outlet이 부착된 컴포넌트에 커스텀 프롭스를 context라는 이름으로 정의하면 된다.

<Outlet context={{ myProps: data }} />

프롭스가 전달되면 중첩 라우트의 자식 컴포넌트에서 useOutletContext 훅을 통해 프롭스를 전달받을 수 있게 된다. (타입스크립트 기반이라면 인터페이스를 제네릭으로 전달해야한다.)

import {useOutletContext} from "react-router";

function Component(){
  const data = useOutletContext<MyGeneric>();
  return <h1>Hello!</h1>;
}

# Reference

  1. nomad coders - React로 영화 웹 서비스 만들기 (opens new window)
  2. YTS movie API docs (opens new window)
  3. React Router v6 튜토리얼 (opens new window)