Dev/JS Family, HTML, CSS

[React] 리액트 심화 - SPA, React Router, 비동기 프로그래밍

youngst 2022. 9. 16. 11:48

SPA와 React-Router

SPA란?

전통적인 웹 어플리케이션의 경우 각 페이지마다 고유한 HTML 페이지를 만들어 페이지를 이동할 때마다 HTML을 로드해야 했다. 때문에 페이지를 이동할 때마다 깜빡임(새로고침)이 발생했고 이는 사용자 경험을 헤친다고 판단되었다.

 

SPA는 Single-page application의 약자로 하나의 HTML 페이지에 앱 실행에 필요한 자바스크립트와 CSS 같은 모든 자산을 로드하는 애플리케이션이다. App에서 페이지 또는 후속 페이지의 상호작용은 서버로부터 새로운 페이지를 불러오지 않기 때문에, 페이지를 이동하더라도 새로고침이 되지 않는다! -> Client Side Rendering

 

Routing

URL에 따라 알맞은 콘텐츠(UI)를 전달해주는 기능. JavaScript의 History API가 이를 가능하게 해준다.

 

React Router

리액트는 UI 라이브러리이기 때문에 라우팅이나 전역 관리 등은 별도의 라이브러리를 사용해야한다. 리액트 라우터는 이를 위해 만들어진 라이브러리이다.

 

<BrowserRouter>

브라우저 라우터는 History API를 이용해 URL 주소를 받아 해당 주소에 맞는 컴포넌트를 새로고침 없이 보여줄 수 있게한다. 별도의 서버 설정을 하지 않으면 새로고침시 경로 에러가 발생할 수도 있다.

 

<HashRouter>

해시 라우터는 브라우저에서 서버로 URL이 전송되면 안 될 때 사용한다. 이는 보통 서버를 완전히 제어할 수 없는 공유 호스팅 서비스 등을 사용할 때 발생한다. 검색엔진 최적화가 되지 않기 때문에 특별한 상황이 아니면 권장되지 않는다.

 

React Router 사용하기

//App.js

function App() {
  // root url : main page component
  // tech url : tech page component
  // blog url : blog page component

  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path={"/"} element={<MainPage />} />
          <Route path={"tech"} element={<TechPage />}>
            <Route path="javascript" element={<JavascriptPage />} />
            <Route path="react" element={<ReactPage />} />
            <Route path="react/:docId" element={<ReactDocPage />} />
          </Route>
          <Route path={"blog"} element={<BlogPage />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

(import 생략)

HashRouter와 BrowserRouter 중 선택하여 사용할 수 있다. 공식 문서에서는 꼭 필요한 경우가 아니라면 브라우저 라우터를 사용하는 것을 권장하고 있다. Route를 이용해 각 경로마다 표시할 컴포넌트를 element에 넣으면 된다.

 

<Link>

리액트 라우터에서 특정 경로로 가기 위해서는 a 태그 대신에 Link 컴포넌트를 사용해야한다. a 태그는 페이지를 새로고침한다.

//MainPage.js
import React from "react";
import { Link } from "react-router-dom";

export default function MainPage() {
  return (
    <div>
      <h1>MainPage</h1>
      <Link to="/blog">Blog</Link> | <Link to="/tech">Tech</Link>
    </div>
  );
}

 

<Outlet>

Outlet은 해당 Route의 자식 Route를 랜더링한다. Route의 element 속성의 기본값은 <Outlet />이다.

import React from "react";
import { Outlet, Link } from "react-router-dom";

export default function TechPage() {
  return (
    <>
      <div>
        <h1>TechPage</h1>
        <Link to="/tech/react">React</Link> |{" "}
        <Link to="/tech/javascript">Javascript</Link>
      </div>
      <Outlet />
    </>
  );
}

 

:docID, useParams

useParams Hook과 콜론(:)을 사용하면 URL로부터 값을 받아올 수 있다. 글 번호 등을 이용하여 서버에서 특정 글을 불러와야할 때 유용하게 사용할 수 있다.

 

useNavigate

Link 태그와는 조금 다르게 특정 URL로 바로 이동할 수 있게 하는 Hook이다. Route 요소 안의 컴포넌트에서 사용할 수 있다.

import React from "react";
import { useParams, useNavigate } from "react-router-dom";

export default function ReactDocPage() {
  const params = useParams();
  console.log(params); // { docID: 1, 2, 3.... }

  const navigate = useNavigate();

  return (
    <>
      <h5 onClick={() => navigate("/")}>logo</h5>
      <div>ReactDocPage ##{params.docId}</div>
    </>
  );
}

 

 

 

비동기 프로그래밍과 API 호출

비동기란?

일반적인 프로그래밍의 경우 함수의 순서에 따라 순차적으로 실행된다. 때문에 시간이 오래걸리는 특정 함수가 있다면 이후에 있는 함수들도 처리가 늦어지고 이는 사용자 경험 저하로 이어진다. 때문에 특정 함수의 경우 순차적으로 진행하는 대신 따로 실행시킬 필요가 있는데 이를 비동기 프로그래밍이라고 한다. 자바스크립트의 경우 비동기 처리는 브라우저 API의 도움을 받아 처리한다.

 

Promise

Promise 말 그대로 비동기 연산이 종료된 후에 어떠한 결과를 제공하겠다는 약속이다. 해당 연산이 진행 중이면 대기(pending) 상태를 가지며, 비동기 연산이 종료되면 이행(fulfilled)되거나 거부(rejected)되거나 둘 중 하나의 상태를 갖는다. 

 

async, await

Promise는 Promise.then을 이용하여 체이닝(chaining)을 통해 비동기 처리를 해야한다. 단순한 연산일 때는 괜찮지만 복잡한 연산의 경우 then 체이닝이 길어지면서 콜백 지옥과 다를바 없는 상황이 발생할 수도 있다. 이를 위한 문법적 설탕으로 자바스크립트에서는 async와 await를 사용할 수 있다.

 

async 키워드를 이용하면 함수는 Promise를 반환한다. 이 함수 내부에서 await를 이용하면 이 Promise가 이행될 때까지 다음 줄로 실행이 진행되지 않는다.

 

fetch API

fetch API는 브라우저에서 기본적으로 제공하는 API이다. 이를 이용하면 API를 이용해 데이터를 쉽게 비동기로 불러올 수 있다. Promise를 반환하기 때문에 then 체이닝이나 async-await을 사용해야 한다!

  useEffect(() => {
    async function fetchData() => {
      const res = await fetch("https://jsonplaceholder.typicode.com/posts");
      console.log(res);
      const result = await res.json();
    }
  }, []);

 

axios

Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트이다. 이를 이용하면 JSON 형식의 데이터를 자동으로 변환해준다. 또한 요청 및 응답을 인터셉트할 수 있다. 추가로 fetch API에서는 불가능한 Timeout 처리가 가능하다!!

 

axios로 Data를 받을 경우 추가 값들과 함께 반환되기 때문에 내부에 data로 접근해서 데이터를 받아야한다.

  useEffect(() => {
    async function fetchData() {
      const result = await axios.get(
        'https://jsonplaceholder.typicode.com/posts'
      );
      console.log(result);
      console.log(result.data);
      return result.data;
    }

    fetchData().then((res) => {
      setDocs(res);
    });
  }, []);

 

Custom Hook

리액트에서는 useState와 useEffect 등을 조합하여 새로운 기능을 갖는 Custum Hook을 만드는 것을 권장하고 있다. 특정한 상태값(state)를 만드는 로직은 전통적으로는 고차 컴포넌트나 render props(props로 함수를 넘겨주는 테크닉)를 사용하였지만 이를 Hook을 이용해서 해결할 수 있다.

 

커스텀 훅은 꼭 use라는 이름으로 시작하는 함수여야한다. 

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

 

useFetch

useFetch는 React에서 기본으로 제공하지 않는 커스텀 훅이다. 구글에 useFetch를 검색해보면 수많은 라이브러리가 존재함을 알 수 있다. 보통 데이터는 fetch는 같은 동작을 반복하기 때문에 Custom Hook으로 만들어 사용하면 재사용성을 높여서 사용할 수 있다. 대형 프로젝트에서는 많이 사용하지는 않는다.

 

swr과 React-Query

비동기 로직을 쉽게 해주는 최신 라이브러리. SWR을 사용하기 위해서는 네이티브 fetch를 이용한 fetcher 함수를 먼저 생성해야 한다.

const fetcher = (...args) => fetch(...args).then(res => res.json())

그 다음 useSWR을 import하여 사용하면 된다.

import useSWR from 'swr'

function Profile () {
  const { data, error } = useSWR('/api/user/123', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  // 데이터 렌더링
  return <div>hello {data.name}!</div>
}

useSWR에는 URL마다 유니크한 key를 만들기 때문에 같은 url로 API를 호출하면 중복해서 호출하지 않는다. 다음과 같이 커스텀 훅을 만들어 재사용성을 높일 수도 있다.

function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}