[JS] 리액트로 해리포터 spell book 만들기(react-router-dom, fetch API)

PEPPERMINT100·2020년 11월 5일
0
post-custom-banner

서론

와 제목은 참 거창하지만 사실 간단한 fetch와 리액트 라우팅으로 해리포터 API를 긁어와서 보여주는 간단한 웹 어플리케이션을 만들 겁니다. 이번엔 react-router-dom의 기본부터 공식 문서를 활용한 응용도 해보겠습니다.
React에서의 Route
일반적으로 JSP, PHP, NodeJS(ejs,pug...), Django(django template)등을 이용하여 서버 사이드에서 템플릿 엔진을 통한 개발을 하면 백엔드의 REST api를 통해 데이터를 get 또는 post 방식으로 받거나 보내고 동시에 그 URL을 통해 화면 단도 보여줍니다. 하지만 React에서 React에서 react-router-dom 라이브러리를 이용하면 프론트단에서 라우팅을 할 수 있게 되고 이는 백엔드과 독립적으로 작용할 수 있게 됩니다. 이로 인해 state의 변화에 의한 렌더링이 실행되는 리액트의 장점을 극대화할 수 있습니다.

설치

yarn add react-router-dom

라우팅

url의 변화에 따라 보여지는 부분을 나누는 방법은 BrowserRouter 또는HashRouter를 이용하여 나눌 수 있습니다.

import React from "react";
import Main from "./components/Main";
import About from "./components/About";
import { BrowserRouter as Router, Route } from "react-router-dom";
function App() {
  return (
    <div className="App">
      <Router>
        <Route path="/" exact component={Main} />
        <Route path="/about" component={About} />
      </Router>
    </div>
  );
}
export default App;

굵게 칠한 부분을 자세히 보면 먼저 Main.js, About.js를 각각 간단히 생성해주고 Route를 통해 path와 component를 정해줍니다. 여기서 path는 접속할 URL 경로를 의미하고 component는 그 path로 접속했을시 렌더링할 component를 정해줍니다. exact 옵션은 정확히 동일한 path에서 정해진 컴포넌트만 출력해주게 하는 옵션입니다. 만약 exact 옵션을 제거하고 /about에 접속하면 Main 컴포넌트와 About 컴포넌트가 모두 렌더링 되는 것을 볼 수 있습니다.

<Router>
   <Route path="/" exact component={Main} />
   <Route path="/about" component={About} />
</Router>

이 부분을 보면 Route을 Router로 감싸준 것을 확인 할 수 있습니다. Router는 코드 상단에서 BrowserRouter의 Alias입니다.
BrowserRouter말고도 HashRouter라는 또다른 라우터가 존재하는데, HashRouter는 URL을 #으로 즉 Hash 시켜서 보여주는 라우터입니다. 이는 정적 페이지에 사용됩니다. 정적 페이지란 이미 정해져있는 파일들로 사용자나 네트워크의 요청 및 응답의 영향이 없이 만들어진 웹 페이지를 의미합니다. 만약 gh-pages와 같은 정적 페이지 배포를 할 때 BrowserRouter를 사용한다면 제대로 동작하지 않게 됩니다.

import React from "react";
export default function Main() {
return (
    <div>
      <h1>Main</h1>
      <Link to="/about">About</Link>
    </div>
  );
}

링크는 다른 라우트로 이동할 수 있게 도와주는 역할을 합니다. to 옵션에 이동하고 싶은 URL을 입력해주면 됩니다. 약간 HTML의 a 태그와 비슷하게 생겼죠? Link는 a 태그(Anchor)처럼 작동합니다. 따라서 a 태그에 적용되는 css 스타일링중 text-decoration 등의 사용이 가능합니다!
그렇다면 차이점은 무엇일까요? 리액트의 장점 중 하나는 조건부 렌더링입니다. state의 변화에 따른 컴포넌트 하나만을 렌더링하여 속도, 보안, 리소스 효율면에서 아주 뛰어납니다. 만약 일반적인 HTML 페이지에서 a 태그를 이용하여 라우팅을 하면 새로운 페이지로 이동할 때 전체 웹 페이지가 로드되지만 Link 를 이용한다면 부분적으로 컴포넌트가 렌더링이 됩니다.
history를 통한 이동
Route가 적용이 된 컴포넌트는 history, match, location를 자동으로 props로서 가지게 됩니다. 아래 코드는 위 Link와 동일한 결과를 보여주는 코드입니다.

import React from "react";
export default function Main({ history }) {
  const onClick = () => {
    history.push("/about");
  };
return (
    <div>
      <h1>Main</h1>
      <button onClick={onClick}>About</button>
    </div>
  );
}

history는 라우팅 경로들을 저장하고 있는 프로토타입입니다.

console.log(history);

를 통해 history 내부를 보면 여러가지 메소드가 저장이 되어 있는데 그중 push는 안에 파라미터로 주어진 URL(“/about”)로 이동을 하고 goBack 메소드는 이전 페이지를 기억해 두었다가 그 페이지로 이동을 시킵니다.

import React, { useEffect } from "react";
export default function About({ history, location, match }) {
  useEffect(() => {
    console.log("history", history);
    console.log("location", location);
    console.log("match", match);
  });
  const onClick = () => {
    history.goBack();
  };
  return (
    <div>
      <h1>About</h1>
      <button onClick={onClick}>GoBack!</button>
    </div>
  );
}

제가 간단히 공부를 위해 작성한 About.js 입니다. historylocation, match를 콘솔에 출력하게 해놓고 자세히 살펴봅니다. 이 후 우리가 사용할 params를 match 안에 존재하는데 일단 그렇다는 것만 알아둡니다.

Open API 가져오기

www.potterapi.com
위 사이트에 접속하고 가입을 하면 Api key를 받을 수 있습니다. 키를 받고 doc를 훑어본 후 돌아옵시다.
Spells.js라는 파일을 만듭니다.
먼저 사이트로 부터 받은 API 키를 정해줍니다.

const API_KEY = "<YOUR_API_KEY>";
const URL = `https://www.potterapi.com/v1/spells?key=${API_KEY}`;

백틱을 이용하여 URL도 적어줍니다. 위에 관련된 내용은 해리포터 API 사이트 doc에 모두 적혀있습니다. 받아온 API 키를 key 옵션에 꼭 입력해야 합니다.(키가 없거나 틀리면 정보를 가져올 수 없습니다.)

import React, { useState, useEffect } from "react";
const [spells, setSpells] = useState([]);
  useEffect(() => {
    getSpells();
  }, []);
const getSpells = async () => {
    const data = await fetch(URL);
    const json = await data.json();
    setSpells(json);
  };

이제 fetch를 통해 URL의 정보를 가져옵니다. 비동기로 작동해야 하므로 async await을 이용합니다. 아래처럼 Promise를 이용해도 좋습니다

fetch(URL)
.then(res=>res.json())
.then(json=>console.log(json))
.catch(err=>console.error(err))

먼저 console.log를 통해 가져온 spells 들을 확인하면 약 150개의 Array를 가져옵니다. 이제 이 Array를 뿌려줄 Spell 컴포넌트를 만듭니다.

import React from "react"; //This is Spell.js not Spells.js
export default function Spell({ spell }) {
  return <div>{spell}</div>;
}

spell이라는 props를 받도록 만들기 때문에 보여줄 때 spell이라는 prop를 잊지말고 주어야 합니다.

return (
    <div>
      <h1>Spells</h1>
      {spells.length === 0 ? ( 
        ""
      ) : (
        <ul>
          {spells.map(({ spell, _id }) => {
            return (
              <li key={_id}>
                <Link to={{ pathname: `/spell/${_id}`, state: { spells } }}>
                  <Spell spell={spell} />
                </Link>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );

굵게 표시된 부분을 보면 Spell 컴포넌트에 잊지 않고 우리가 불러온 데이터를 spell이라는 이름으로 전해주었습니다.
위와 같이 Spells.js의 리턴 부분을 작성해줍니다. 먼저 spells 데이터를 가져오기 전에 spells를 렌더링하지 않도록 삼항 연산자를 이용합니다. 그리고 fetch가 성공하면 map을 통해 모든 스펠들을 보여줍니다.
리액트에서 map 이용은 key prop이 꼭 필요하므로 해리포터 API에 존재하는 _id를 통해 정해주었습니다.
그리고 이제 Link를 보면 처음에 배운 것과 조금 다르게 생긴 것을 볼 수 있습니다.
문서를 확인하면

<Link
  to={{
    pathname: "/courses",
    search: "?sort=name",
    hash: "#the-hash",
    state: { fromDashboard: true }
  }}
/>

이러한 부분을 확인 할 수 있습니다. 링크에서 pathname에 이동하고 싶은 URL을 넣어주면 되고 state 옵션을 통해 데이터 역시 보내줄 수 있습니다!
따라서 우리가 fetch해온 배열들을 전부 하위 컴포넌트인 Spell에도 보내줍니다. 왜 이렇게 하냐면 우리는 보여준 스펠을 클릭하면 바로 그 스펠의 디테일을 보여주게 하기 위해서 입니다.
뭔가 굉장히 복잡하게 설명이 되었는데 부분부분 코드를 보여드렸는데 전체 코드는 글 맨 아래에 첨부되어 있습니다!
참고로 App.js(BrowserRouter가 있는 컴포넌트)에

<Route path="/spells" component={Spells} />

를 추가해주는 것을 잊지마세요.
useParams를 이용한 동적 URL
App.js(BrowserRouter가 있는 컴포넌트)에

<Route path="/spell/:id" component={SpellDetail} />

다음과 같은 라우팅을 해줍니다. 물론 SpellDetail이라는 컴포넌트를 만들고 import도 해주도록 합니다.
여기서 :id는 params라는 이름으로 동적으로 변화하는 URL이 됩니다.

Link to={{ pathname: `/spell/${_id}`, state: { spells } }}>
   <Spell spell={spell} />
</Link> //This is part of Spell.js\

Spells.js에서 작성한 Link를 보면 pathname을 백틱으로 이용하여 _id를 준 것을 확인할 수 있습니다. 불러온 spells를 보면 _id라는 이름으로 고유번호가 들어가 있습니다. 이 고유 번호를 통해 각 스펠마다 각기 다른 내용은 렌더링할 수 있게합니다.

import React, { useEffect, useState } from "react";
import { useParams, useLocation } from "react-router-dom";
export default function SpellDetail() {
  const data = useLocation().state.spells;
  const id = useParams();
  const [spell, setSpell] = useState([]);
const getSpell = () => {
    const spellData = data.filter(data => data._id === id.id);
    setSpell(spellData);
  };
useEffect(() => {
    getSpell();
  }, []);
  return (
    <div>
      <h1>SpellDetail</h1>
      {typeof spell[0] === "undefined" ? (
        ""
      ) : (
        <section>
          <p>spell : {spell[0].spell}</p>
          <p>type : {spell[0].type}</p>
          <p>effect : {spell[0].effect}</p>
        </section>
      )}
    </div>
  );
}

위와 같이 SpellDetail.js를 작성합니다. 원래는 location, history, match.params 등을 props를 통해 전해주어야 하지만 react-router v5 이후 등장한 useParams와 useLocation을 이용하겠습니다. 이는 props.location과 props.match.params와 동일하게 이용 됩니다.

const data = useLocation().state.spells;

을 이용하면 상위 컴포넌트에서 Link의 옵션을 통해 전달받은 데이터에 접근할 수 있게 됩니다.

const id = useParams();

useParams를 이용하면 :id로 인해 동적으로 생성된 파라미터에 접근할 수 있데 됩니다. 이 둘을 이용하여 filter 메소드로 우리가 클릭한 Spell을 찾아서 보여줄 수 있게 됩니다!
결론
리액트 라우터를 이용하면 프론트에서 라우팅을 가져갈 수 있게 되어 업무 분담화가 용이해지고 어떤 URL로 get 또는 post 방식을 이용하는지 사용자에게 보여주지 않아도 됩니다. 이는 개발, 유지 보수의 영역, 보안적인 영역에서 더 좋은 방식입니다. 리액트와 같이 프론트 엔드 라이브러리 및 프레임워크의 발전은 조금씩 백엔드와 프론트엔드의 영역을 확실히 해주고 있습니다. 이제 프론트에서 조금 더 넓은 영역의 작업을 처리하고 백엔드는 데이터와 API만을 전송하고 서버리스 아키텍쳐에 대한 부분으로 활동 범위를 확장하는 쪽으로 웹 개발이 진화하고 있다고 생각합니다.

전체 코드

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.
post-custom-banner

0개의 댓글