Web: Single Page Application, SPA와 React, 그리고 404

Heechan
HcleeDev
Published in
18 min readDec 3, 2022
Photo by Kelly Sikkema on Unsplash

저번에 우리 회사 서비스에 어떤 장애가 발생해서, 아닌밤중에 급하게 원인을 찾아야 하는 일이 있었다. 화면이 아무것도 보이지 않던 장애, 뭔지 모르겠어서 네트워크 탭을 확인해보니 다짜고짜 404 에러가 하나 눈에 들어왔다. 심지어 에러 로깅 시스템인 Sentry에서는 webpack 뭐시기가 찍혀있었다. 그래서 이거 뭔가 webpack에서 빌드가 잘못되었다고 착각을 하고 배포 전으로 롤백을 결정했다.

물론 롤백하니까 해결은 됐지만… 사실 원인은 그게 아니었다. 404 에러가 뜨는 원인에 대해서 정확히 알고 있었다면 좀 더 제대로 확인해보았을텐데, 그걸 몰랐어서 팀장님한테 폐를 끼쳐버렸다.

그래서 이번주는 왜 404가 떴는지, Single Page Application에 대한 개념과 React에서는 SPA를 어떻게 구현하는지 간단히 알아보자.

웹 서비스의 역사와 기존 방식의 불편한 점

웹의 역사를 떠올려보자.

처음에 웹은 문서의 공유, 정보의 공유의 기능을 했기 때문에, 웹 페이지에 별다른 기능이 있을 필요는 없었다. 이때는 지금보다 정적인 HTML, CSS로 만들어진 웹 페이지에서, 말그대로 Markup 처리된 문서와, 어떤 하이퍼 링크 버튼을 누르면 다른 링크의 다른 문서로 이동하는 정도였을 것이다.

이러면 어떤 링크에 들어가면 웹 서버로부터 HTML 파일이 매번 돌아오고, 그 문서를 브라우저가 보여주고, 페이지에 사용자와 인터랙션은 딱히 없었다.

웹 서비스는 워낙 잠재력이 큰 만큼, 뭔가 사용자와 상호작용하는 기능에 대한 요구가 생겼다.

이때 JavaScript가 웹 세계에 등장한다. 이제 웹 서버에 웹 페이지를 요청하면 HTML, CSS 뿐만 아니라 JavaScript에 대한 파일도 함께 보내준다. 이 JavaScript 화면이 HTML과 연결되어 JavaScript 기능을 이용해 DOM을 조작하거나, 서버에 필요한 데이터를 요청할 수 있게 되었다.

하지만 이때도 웹 서비스는 JavaScript가 주가 되지 않았다. 결국 HTML이 기본이 되고, JavaScript는 살짝의 동적인 기능을 위해 곁들어지는 느낌이다.

이러면 결국 페이지가 전환될 때마다, 즉 브라우저 상단 주소창에 적힌 링크가 변경될 때마다 웹 서버로부터 HTML, CSS, JavaScript 파일을 다시 받아와야 한다. 웹 서버는 적당히 사용자가 원하는 정보에 맞춰 HTML을 구성해서 되돌려줘야 하는데, 당연히 문제가 있다.

많은 사용자의 많은 요청을 받은 웹 서버 입장에서는 매번 그에 맞는 HTML을 구성해서 보내주는 처리를 하는 것도 굉장히 힘들다.

그리고 사용자 입장에서도 나는 예를 들어 화면 한쪽 코너만 변하는 것을 기대하고 눌렀는데 링크가 바뀌고, 전체 HTML, CSS, JS가 다시 랜더링되는 것을 기다려야 하는 상황이 오면 좋은 경험을 할 수 없다. 엄청 어릴 때 생각해보면 뭐 할 때마다 흰색 화면이 됐다가 커서 로딩이 빙글빙글 돌아간 후 화면이 나왔던게 아마 이런 과정이 아니었을까 싶다.

Single Page Application, SPA

점점 더 많고 다양한 서비스를 제공해야 하는 웹 서비스는 이런 단점을 극복해야했다. 이 문제를 해결하기 위해 00년대 중반부터 제시된 방법론이 Single Page Application, 즉 SPA다.

요즘 웹사이트들을 생각해보면, 뭔가 버튼을 눌렀다고 아예 화면 전체가 새로고침되는 경우는 없다. 아래 움짤을 보자.

이건 네이버의 Dooray 메일 서비스인데, 기존에는 오른쪽에 메일 본문이 나오지 않다가, 내가 선택하자 현재 주소가 변경되고 메일 본문이 로딩되어서 오른쪽에 랜더링되는 모습이다. 페이지의 다른 부분(Navigation Bar라든가… 메일 리스트라든가…)은 새로 로딩되지 않는 것을 확인할 수 있다.

SPA를 이용하면 하나의 페이지 내에서 다양한 사용자 상호작용과, 그때그때 필요한 데이터만 따로 서버로부터 로딩해주는 방식으로 진행된다.

이런게 어떻게 되는걸까? 아까 JavaScript가 등장했을 때, JS는 DOM을 조작할 수 있고 필요한 정보를 가져올 수 있게 되었다고 말했었다.

SPA에 이르러서는 HTML 중심이 아닌 JavaScript가 중심이 되어 웹 페이지를 구성한다. HTML 파일은 최소한으로 하고(Single Page), 그 페이지를 하나의 도화지 삼아 JavaScript가 내용을 채워넣을 수 있다.

기존과의 차이점을 보여주는 좋은 움짤이 있어서 가져왔다.

출처: https://www.bloomreach.com/en/blog/2018/what-is-a-single-page-application?spz=article_orig

기존에는 웹 서버에서 .jsp 파일을 가지고, 그 파일에 있는 로직을 따라 서버의 데이터베이스에 접근해 HTML을 구성한 후 클라이언트로 보내주는 방식이었다.

출처: https://www.bloomreach.com/en/blog/2018/what-is-a-single-page-application?spz=article_orig

Single Page Application에서는 서버에서 .jsp 같은 파일을 들고 있는게 아니라, 최초 로딩 때 클라이언트에 HTML, CSS 그리고 JS를 한 번에 다 보내준다. 그리고 데이터베이스로부터 와야 하는 정보는 그 이후에 따로 서버에 요청해서 로드한다. 이 재료들을 다 합쳐서 화면을 구성해서 사용자에게 보여준다.

만약 저 움짤에서 화면에 나오는 나무의 모양을 바꾸고 싶으면(아까 봤던 메일 서비스에서 메일 본문을 보고 싶으면… 정도로 생각할 수 있겠다), 기존에는 서버에 새로운 요청을 하고 아예 새로운 페이지를 받아야했다. 하지만 SPA에서는 그냥 필요한 데이터만 서버한테 요청해서 받은 후, 그 데이터를 JavaScript를 이용해 DOM에 끼워넣어주면 되기 때문에 굳이 페이지 전체를 새로고침할 필요가 없어졌다.

Single Page Application이 대세가 되면서, 백엔드에도 영향이 있었다.

기존에는 웹 서버에서 DB에서 데이터를 가져와 HTML을 구성 후 보내주는, 거의 완제품을 나눠주는 방식이었기에 서버 개발자가 jsp , jQuery 를 다뤄야 하는 경우도 많았다고 한다.

하지만 SPA에서는 웹 서버는 최초 로딩 때 HTML, CSS, JS를 묶어서 한 번에 보내주는 역할만 하고, 나머지 구성은 클라이언트인 브라우저가 알아서 할 것이기 때문에 호스팅의 역할이 강해졌다.

그리고 백엔드는 API 서버 를 구성하게 된다. 흔히 말하는 REST API 방식을 통해, 웹 브라우저에서 돌아가는 JavaScript는 API 서버에게 이런 정보를 주세요~ 라고 요청하고, 그 응답을 보내준다. 위에서 말한 SPA가 따로 데이터를 받는다는 것이 다 이 방식이다.

따라서 SPA가 퍼지면서 백엔드와 프론트엔드 개발자의 역할도 확실히 분리되게 되었다고 볼 수 있겠다.

개인적으로는 ‘Application’ 개념으로 나아갔다는 면도 꽤 주목할만한 부분인 것 같다.

그 이전에는 웹은 개발자도 사용자도 Page 단위로 인식하고 있었을 것 같다. 아마 대부분 사용자는 지금도 그런 느낌으로 인식하고 있을 것 같긴 한데…

마치 휴대폰 애플리케이션이나 PC의 응용 프로그램들이 각자의 클라이언트에서 필요한 요소를 다 가지고 있고 데이터의 경우는 통신을 통해 가져오는 것처럼, 웹의 경우에도 브라우저가 그 클라이언트 역할을 하면서 동작하는 웹 애플리케이션이 되었다고 생각할 수 있을 것 같다.

SPA의 장점과 단점

SPA의 장점은 크게 한 가지로 퉁 칠 수 있다. 전반적인 유저 경험이 좋아진다.

위에서도 대강 다 얘기한 내용이긴 한데, 간단히 이유들을 짚어보고 가자면…

  • 한 번에 HTML CSS JS를 로드한다

최초 한 번만 HTML, CSS, JS를 다운 받으면 그 이후에는 다시 다운로드 받지 않아도 된다.

  • 서버에 드는 비용이 줄어들 수 있다

매번 HTML들을 적절히 만들어야 하는 부담을 지고 있던 서버가, 이제는 간단히 저장된 HTML, CSS, JS를 묶어서 보내주기만 하면 되기 때문에 부하가 좀 줄어든다.

  • 반응이 빠르다

최악의 경우 아예 새로고침을 해야 했던 기존 방식과 달리, SPA는 API 서버로부터 데이터만 새롭게 받아오는 딜레이 정도만 있으면 화면을 볼 수 있다.

단점이 없는건 아니다.

  • 처음 로딩이 느리다

중간중간 로딩을 없애줬는데 처음 로딩이 느린건 어쩔 수 없지 않나 생각할 수도 있겠지만, 많은 개발자들은 이 처음 로딩조차 빠르게 만들고 싶어한다.

처음에 HTML, CSS, JavaScript를 받아오고, 그리고 JavaScript를 통해 화면을 랜더링하는 동안 걸리는 딜레이가 있다. 이를 줄이기 위해 Code Splitting 같은 기법을 적용하기도 하고, Server Side Rendering 방식을 취하기도 한다. 어찌 잘 서버를 벗어났는데 어째서 다시 서버 사이드 랜더링을 하나 하겠지만… 유저가 최대한 빨리 화면의 틀이라도 볼 수 있도록, 동적 데이터를 제외한 부분은 미리 만들어서 HTML로 제공하기도 한다.

  • SEO 문제가 있다

검색 엔진에서 문서를 볼 때 index.html 같은 HTML 파일을 확인할텐데, 기존 방식대로면 HTML 파일에 데이터 내용까지 이미 다 들어가있으니 검색 엔진 봇이 수집하기에 용이하다.

하지만 차후 데이터를 API 서버로부터 받고 JavaScript를 통해 DOM을 조작하는 SPA는 검색엔진 최적화가 안되어있기 때문에 따로 처리를 해줘야 하는 단점이 존재한다.

React에서의 SPA, react-router-dom

React는 대표적인 SPA 프레임워크다. 하나의 페이지만 사용하고 나머지는 내부 내용은 React에서 알아서 랜더링을 진행한다. JavaScript가 이를 수행한다고 보면 된다.

현 시점에서 create-react-app을 이용해 앱을 만들고 나서 index.tsx 를 확인해보면 이런 코드가 나온다.

import React from 'react'; 
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);

root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

여기서는 ReactDOM.createRoot , root.render 를 이용해서 document로부터 root를 가져온 후 거기다가 우리의 React Node들을 랜더링하는 방식으로 간다. 아마 React가 내부에서 랜더링해서 DOM에 끼워넣는 조작을 할테니, 이를 JS가 SPA 역할을 수행한다고 보면 될 것 같다.

근데 root 는 어디있을까?

빌드해보면 나오는 결과물인 index.html 에서 확인할 수 있다.

<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Web site created using create-react-app"/>
<link rel="apple-touch-icon" href="/logo192.png"/>
<link rel="manifest" href="/manifest.json"/>
<title>React App</title>
<script defer="defer" src="/static/js/main.4d45a67d.js"></script>
<link href="/static/css/main.503e2b8a.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

빌드되어 나오는 HTML 파일을 확인해보면, body 에는 정말 특별할게 없다. <div id="root"> 만 있을 뿐 추가적인 요소는 없다.

다만 head 에서는 script 를 통해 우리의 JS 파일을 가져오고 있는데, 이를 이용하면 저 위에서 만든 index.tsx 가 불러져와 동작할 것이다.

그러면 최초 진입 시 이 index.html 만 주더라도 css, js 파일을 가져와 JS가 알아서 DOM을 다 조작해주기 때문에, React는 이런 식으로 SPA로 동작한다고 볼 수 있다.

하지만 SPA를 하면 이것말고도 신경써야 할 게 있다. 바로 브라우저의 주소창에 있는 주소값을 어떻게 할지 문제로, 현재 location을 어떻게 판단할지, 이거의 변화는 어떻게 감지할지가 핵심 문제가 된다.

그런 문제는 react-router-dom이 해결해준다.

react-router라는 프로젝트에 포함된 녀석인데, 웹에서만 사용할 것이라면 react-router-dom을 중심으로 써도 무방하다. 다른거 하나는 React Native에서 쓰는걸로 보인다.

간단히 방식정도만 알아보도록 하자.

import React from 'react'; 
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);

root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

BrowserRouter 를 가져와서 가장 상위에 한번 씌워줘야 한다. 그 아래에 있는 컴포넌트들은 이제 BrowserRouter 컴포넌트가 가지고 있는 브라우저 주소값에 대한 정보 등에 접근할 수 있다.

그리고 그 내부에 있는 App을 react-router-dom을 이용해 살짝 변형해보자.

만약 주소가 / 라면 메인 페이지를, /mypage 라면 마이페이지를, /user/123 이라면 123번 유저에 대한 정보를 보고 싶다고 생각해보자.

const App = () => {
return <Routes>
<Route path="/" element={<MainPage />} />
<Route path="/mypage" element={<MyPage />} />
<Route path="/user/:id" element={<UserInfo />} />
</Routes>
};

이렇게 사용하면 사용법은 간단하긴 하다. <Routes> 안에 각 페이지를 담은 <Route> 를 만들었다. 대충 이렇게만 봐도 어떤 느낌인진 알 수 있을 것 같은데, 특이한 점은 /user/:id 다.

이렇게 작성하면 id 라는 Parameter가 주소의 해당 위치로 들어온다고 본다. /user/123 이라면 id: 123 이 되는 것이다.

하위 컴포넌트에서는 react-router-dom이 제공하는 Hook인 useParams를 이용해 const { id } = useParams() 이렇게 불러올 수 있다.

그리고 다른 주소로 옮기는 하이퍼링크 버튼을 만들 때 react-router-dom에서는 기본적으로 <Link> 를 사용한다.

const MainPage = () => {
return <div>
<Link to="/mypage"><p>마이페이지 가기</p></Link>
</div>
};

이런 식으로 링크를 만들 수 있다.

느낌은 a href 를 사용하는 것과 크게 안 다르다고 생각할 수 있으나, SPA의 장점을 가지기 위해서는 <Link> 를 써야 한다. a href 로 주소가 변경되거나 새창에서 열릴 경우 다시 HTML, JS 파일을 다운 받아야 하나, <Link> 를 사용하면 페이지를 새로고침하지 않고 유지하면서 HTML navigate API를 이용해 주소 값만 바꿔주는 방식으로 동작하기 때문이다.

react-router-dom의 V6에 대한 소개나 HTML의 Navigate API (예전에는 History API였다)에 대한 공부도 조만간 다루면 좋을 것 같다. 좀 더 자세하게는 그때 알아보는걸로…

메인 페이지가 아닐 때 404 에러가 뜬다면

그래서 결국 돌고 돌아 서론에서 말했던 문제로 돌아왔다. 메인 페이지로 접근하지 않으면 HTTP 404 에러가 발생하는 경우가 있다.

예를 들어 나는 example.com/user 에 접근하고 싶어서 주소창에 해당 주소를 치고 들어갔더니 에러가 발생하고, 대신 example.com 으로 접근 후 거기서 유저 링크를 누르니 example.com/user 화면이 잘 나오는 현상이 있는 경우다.

이는 어떤 SPA 프레임워크에서든 발생할 수 있는 문제다.

웹 호스팅 서버 쪽의 세팅이 저렇게 되어있다면, 가장 메인 페이지에 접근할 때만 index.html 을 전달해준다. 만약 유저가 다른 주소로 접근할 경우에는 그 주소에 해당하는 HTML 파일이 없으므로 404 에러를 돌려준다.

SPA에서는 기본적으로 주소값에 따른 변화를 React 내부, 즉 JavaScript에서 관리하는 것이다보니 그냥 웹 호스팅 서버는 알 길이 없다. 메인 페이지에서 HTML, JS 파일을 다 받아왔을 때는, 메인 페이지 내에서 주소를 바꾸면 react-router-dom이 알아서 Route에 맞춰서 변경해준다. 하지만 직접 주소를 쳐서 구체적 주소에 접근하거나 새로고침을 할 경우 서버로부터 HTML, JS 파일을 받아올 수 없어 문제가 생기는 경우다.

출처: https://github.com/remix-run/react-router/issues/5065#issuecomment-298881238

이런 경우 웹 호스팅 서버의 세팅을 바꿔줘야 한다.

어떤 주소로 들어오더라도 index.html 을 돌려주도록 세팅하거나, 정해진 주소로 오지 않으면 index.html 을 찾아갈 수 있도록 Fallback을 응답해주는 방식으로 설정하면 된다. 그러면 처음에는 404 에러가 발생하더라도 어차피 index.html 을 받아올 수 있기 때문에 문제가 해결된다.

받아오고 난 뒤에는 현재 주소값에 따라서 react-router-dom이 알아서 화면을 보여줄 것이다.

결론

SPA가 뭔지 어렴풋이 알 뿐 정확히 React가 어떻게 동작하고 있는지는 알고 있지 못했던 것 같다. 역시 개발자는 장애버그한테 맞으면서 자란다는 생각도 든다.

실제로 웹 서버 세팅을 내가 만져본 적은 없지만, 다음에 팀장님한테 우리 프로덕트는 어떤걸 이용해서 이걸 처리했냐고 한번 물어봐야겠다.

참고한 것

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science