Javascript/React

#1. Single Page Application

rivercity310 2021. 12. 22. 11:27

 

Single Page Application(SPA)은 초기 요청 시 서버에서 첫 페이지를 처리하고 이후의 라우팅은 클라이언트에서 처리하는 웹 애플리케이션입니다.


전통적인 방식의 웹 페이지는 페이지를 전환할 때마다 서버에서 렌더링 결과를 받기 때문에 화면이 깜빡이는 단점이 있습니다. 하지만 SPA는 페이지 전환에 의한 렌더링을 클라이언트에서 처리하기 때문에 마치 네이티브 애플리케이션처럼 자연스럽게 동작하도록 구현할 수 있습니다.



SPA가 구동되는 과정

  • SPA에서는 웹 페이지가 클라이언트에 의해 렌더링된다.
  • 클라이언트 측에서 Javascript(주로 axios 혹은 XMLHttpRequest 객체)와 URL을 통해 서버에게 데이터를 요청하고 응답받는다.
  • 서버는 URL을 통해 들어온 요청에 대한 응답만 해주면 되고, 메서드를 살핀다.
  • 메서드 종류로는 GET, POST, PATCH, PUT, DELETE 등이 있다. (REST API)

 



클라이언트에서 데이터를 요청할 때 AJAX를 사용하고, 그 결과로 서버 쪽에서는 결과물로 JSON 코드를 전송해줍니다. (JSON은 Javascript Object Notation이란 의미로 직역하면 자바스크립트 객체 표기법)


SPA의 단점?

  • 규모가 커질 수록 Javascript의 역햘이 막중해지고 파일의 규모가 너무 커진다.
    (이를 해결하기 위해 동적 임포트와 같은 코드 스플리팅 기법을 사용한다.)
  • Javascript를 실행하지 않는 일반 크롤러에서 페이지의 정보를 수집해가지 못하므로 검색 엔진에 의해 드러나지 않는다.
    (다행히 Chrome은 크롤러가 Javascript를 실행해준다.)

위 단점들은 SSR(Server-side Rendering)을 통해 해결이 가능합니다.



라우팅 처리

만약 localhost:3000에서 localhost:3000/write로 이동할 경우, 페이지는 HTML 하나인데 어떤 방식으로 url을 이용하길래 Javascript만으로 HTML 파일 내용들을 바꿀 수 있는 것일까요? 바로 라우터를 이용합니다. 라우터는 다른 주소에 다른 화면을 보여주는 기능입니다. 대표적인 라이브러리로는 리액트 라우터와 리치 라우터 그리고 next.js가 있습니다.




원시적으로 SPA 구현해보기

SPA를 구현하기 위해 Javascript에서 제공하는 브라우저 API를 사용할 수 있습니다. React에서는 SPA를 구현하기 위해 react-router-dom 패키지를 사용하죠. react-router-dom 역시 내부적으로 Javascript의 브라우저 API를 사용하고 있습니다.


SPA 구현을 위한 다음 두가지 조건이 있습니다.

  • 브라우저의 뒤로 가기와 같은 사용자 페이지 전환 요청을 Javascript에서 처리할 수 있다. 이때도 브라우저는 서버로 요청을 보내지 않아야 한다.
  • Javascript에서 브라우저로 페이지 전환 요청을 보낼 수 있다. 단 브라우저는 서버로 요청을 보내지 않아야 한다.



위와 같은 조건을 만족하는 브라우저 히스토리 API(pushState, replaceState, popstate)를 이용하여 SPA를 구현해 보겠습니다. API 이름에서도 알 수 있듯이 브라우저에는 히스토리에 state를 저장하는 스택(stack)이 존재합니다.

import React, {useEffect, useState} from "react";

export default function App() {
    const [pageName, setPageName] = useState("");
    useEffect(() => {
        window.onpopstate = function(event) {
            console.log(`location: ${document.location}, state: ${event.state}`);
            setPageName(event.state);
        }
    }, []);

    function onClick1() {
        const pageName = "page1";
        window.history.pushState(pageName, "", "/page1");
        setPageName(pageName);
    }

    function onClick2() {
        const pageName = "page2";
        window.history.pushState(pageName, "", "/page2");
        setPageName(pageName);
    }

    return (
        <div>
            <button onClick={onClick1}>
                page1로 가기
            </button>
            <button onClick={onClick2}>
                page2로 가기
            </button>
            {!pageName && <Home/>}
            {pageName === "page1" && <Page1/>}
            {pageName === "page2" && <Page2/>}
        </div>
    )
}

function Home() {
    return <h2>여기는 Home</h2>;
}

function Page1() {
    return <h2>여기는 Page1</h2>;
}

function Page2() {
    return <h2>여기는 Page2</h2>;
}


위 작성된 코드를 npm start를 통해 실행하고 개발자 도구의 네트워크 탭에 가서 서버 요청을 확인해보면 초기 접속에만 서버에서 데이터를 받아오고 이후 버튼을 눌러도 서버로 요청이 가지 않습니다. 단지 스택에 state가 쌓일 뿐입니다.

브라우저의 뒤로가기 버튼을 누를 때마다 onpopstate 이벤트 리스너가 호출되는 것을 확인할 수 있습니다. 계속해서 누르면 stack이 비워질 때까지 onpopstate가 호출됩니다. 이렇게 pushState, replceState, popstate 이벤트만 있으면 클라이언트에서 라우팅 처리가 되는 단일 페이지 애플리케이션을 구현할 수 있는 것입니다.

 

 


 

 

react-router-dom 사용하기


브라우저 히스토리 API를 이용해서 페이지 라우팅 처리를 직접 구현할 수도 있지만 상당히 신경써야 할 부분이 많습니다. React에서는 react-router-dom으로 쉽게 라우팅 처리를 할 수 있습니다. 역시 react-router-dom도 내부적으로는 브라우저 히스토리 API를 사용합니다.

npm install react-router-dom


react-router-dom이 v6으로 업데이트 되면서 기존의 많은 것들이 바뀌었습니다.

  • Switch -> Routes로 대체
  • component, render, exact 삭제 -> element
  • 절대경로 -> 상대경로
  • match -> useParams()
  • Outlet

 

import React from "react";
import {BrowserRouter, Routes, Route, Link, useParams} from "react-router-dom";
import Rooms from "./Rooms";

export default function App() {
    return (
        <BrowserRouter>
            <div>
                <Link to="/">홈</Link><br/>
                <Link to="Photo">사진</Link><br/>
                <Link to="rooms">방 소개</Link><br/>

                <Routes>
                    <Route path="/" element={<Home/>} />
                    <Route path="/photo" element={<Photo/>} />
                    <Route path="/rooms" element={<Rooms/>}>
                        <Route path=":roomID" element={<Room/>} />
                    </Route>
                    <Route path="*" element={<NotFound/>} />
                </Routes>
            </div>
        </BrowserRouter>
    )
}

function Room() {
    // {roomID: "blueRoom"} 혹은 {roomID: "redRoom"} 반환
    const {roomID} = useParams();
    return <h2>{roomID}방을 선택하셨습니다.</h2>;
}

function Home() {
    return <h2>Home!</h2>
}

function Photo() {
    return <h2>Photo!@</h2>
}

function NotFound() {
    return <h2>404 Page Not Found</h2>
}


// Rooms.js
import React from "react";
import {Link, Outlet} from "react-router-dom";

export default function Rooms() {
    return (
        <div>
            <h2>방을 소개할게요</h2>
            <Link to="blueRoom">파란 방이에요</Link><br/>
            <Link to="redRoom">빨간 방이에요</Link><br/>

            <Outlet />
        </div>
    )
}