FE/Next.js

[Next.js] App Router는 SPA(Single Page Application)인가?

unhandled 2025. 5. 20. 15:10

 

MPA(Multi Page Application)

SPA 등장 이전의 방식(JSP, vanilla JS(+jquery)) 은 MPA(Multi Page Application)이었습니다. 이 방식은 페이지 이동 시 서버에서 새로운 HTML을 직접 그려서 클라이언트로 보내줍니다.(이것이 전통적인 SSR입니다.) 페이지 전체를 새로 로드하니 필연적으로 시각적인 깜빡임이 있을 수밖에 없었습니다. 

 

SPA(Single Page Application)

리액트, 뷰, 앵귤러 등 SPA 라이브러리(또는 프레임워크)의 등장은 극적인 패러다임의 변화를 가져왔습니다. SPA의 등장은 사용자 경험의 향상과 개발 편의성을 크게 안겨주었고, 이 시기부터 프론트엔드 분야는 정말 독자적인 전문성을 본격적으로 가질 수 있게 되었습니다. 하지만 SEO 등 기존 SSR의 특징적인 이점을 상실하게 되었습니다.

 

Next.js는 SPA인가 MPA인가

그러면 Next.js는 리액트 기반 프레임워크니 SPA인 걸까요? 아니면 SSR이 가능하니 MPA인 것일까요? 

 

이 두 가지 방식이 가진 특징을 나열하고 그 특징에 Next.js가 해당하는지 살펴보도록 하겠습니다. 이 특징은 기존의 SPA와 MPA이 가졌던 특징입니다. 용어의 절대적인 정의가 아닙니다. 

 

App Router === SPA ?

Next.js는 기존의 SPA가 가진 특징에 부합할까요?

 

SPA의 특징 1. 하나의 페이지(index.html)만 존재한다.

→ Next.js는 이에 해당하지 않습니다.

 

리액트는 오로지 index.html이라는 하나의 페이지만 가지고 있습니다. 최초 접속 시에도 이 index.html이라는 하나의 페이지를 기준으로 클라이언트에서 렌더링(CSR)합니다. 하지만 next.js는 index.html만을 가지고 있지 않을 수도 있습니다. 최초 진입 경로가 어디냐에 따라 서버로부터 전송받는 HTML은 index.html이 될 수도, project.html이 될 수도, 1.html이 될 수도 있습니다. 

 

SPA의 특징 2. 페이지 이동시 서버에서 받은 새 데이터로 현재 페이지를 새로고침 없이 동적으로 다시 작성한다. 

→ Next.js는 이에 해당합니다.

 

Next.js는 새로고침 없이 /about → /project로 페이지를 이동할 수 있습니다. Link 컴포넌트나 next/navigation의 router와 같은 클라이언트 사이드 내비게이션 기능을 사용하기 때문입니다. 실제로 그 페이지를 전부 새로 가져오는 게 아닙니다. 변경할 데이터만 가져와서 갈아 끼우고 상단 주소를 바꾸는 것일 뿐입니다. Next.js는 SPA의 부드러운 페이지 전환이라는 이점을 분명히 가지고 있습니다. 

 

App Router === MPA ?

Next.js는 기존의 MPA가 가진 특징에 부합할까요?

 

MPA의 특징 1. 여러 페이지(xxxx.html)가 존재할 수 있다.

→ Next.js는 이에 해당합니다.

 

좀 더 정확히 말한다면 무조건 이렇다는 건 아니고( / 외에 다른 경로가 존재하지 않거나 다른 경로는 전부 동적 렌더링되는 경우 제외) 여러 페이지(HTML)를 가질 수도 있다는 것입니다. 정적 렌더링 페이지(SSG)의 경우 빌드타임에 그 페이지에 대응되는 HTML을 생성합니다. 그래서 구조적으로는 맞다고 한 것입니다.

 

MPA의 특징 2. 페이지 이동시 "항상" 서버로부터 새 HTML을 받아온다.

→ Next.js는 이에 해당하지 않습니다.

 

서버로부터 새 HTML을 받아올 수는 있지만 "항상"은 아니기 때문입니다. 이 경우는 주소창에 직접 주소를 입력해서 들어가거나 새로고침을 하는 경우, 즉 웹사이트 최초 진입 시에만 해당합니다. 순수 리액트도 새로고침하거나 주소창에 주소 입력해서 들어가는 거랑 똑같은 맥락입니다. (다만 Next.js의 이 경우 발생하는 렌더링 방식은 전통적인 SSR이거나 빌드타임에 생성된 SSG HTML을 받아오는 것이고 리액트는 전통적인 CSR인 것이죠.)

 

만약 Next.js App Router로 구축된 웹사이트에 최초 접속하여 랜딩 페이지인 / 페이지에서 router.push로 /review/3 페이지로 이동하고 Link를 통해 이번엔 /project 페이지로 이동했다고 가정해 봅시다. 방금의 과정에서 처음 접속 이후론 실제로 서버에서 새로운 HTML을 받은 적은 한 번도 없습니다. 첫 접속 이후로는 클라이언트 사이드 내비게이션을 통해 내용물만 교체하고 있는 것입니다. (하지만 서버로부터 payload의 형태로 데이터를 계속 받아 교체합니다. 이건 이동하는 페이지가 빌드타임에 정적으로 렌더링되었든 온디맨드 형식으로 동적으로 렌더링되었든 상관없습니다. 빌드타임에 생성된 HTML은 클라이언트 사이드 내비게이션을 한다면 더 이상 클라이언트로 전송되지 않습니다.) 즉, Next.js는 이 과정에서 단 하나의 HTML 파일을 받아왔고 그 내용을 계속 갈아 끼우면서 사용하고 있는 것입니다. 이 관점에서 Next.js는 SPA의 특성과 정확하게 일치합니다.  

 

MPA의 특징 3. 페이지를 넘어갈 때 "항상" 서버는 HTML을 실시간으로 "그려서" 보내준다.

→ Next.js는 이에 해당하지 않습니다. 

 

서버로부터 새 HTML을 받아올 수는 있지만 요청을 받은 서버가 항상 직접 "그려서" 보내는 것은 아닙니다. 만약 주소창 링크로 직접 접속한 페이지가 정적 렌더링 방식으로 구현된 페이지였다면 요청을 받은 서버가 직접 실시간으로 그려서 보내주는 것이 아닌 빌드타임에 그려놓았던 HTML을 보내줍니다. 요청을 받은 서버가 즉시 HTML을 그려주는 것은 고전적인 SSR 즉, 온디맨드 방식입니다. 

 

공식에 가까운 것은 특징 2

하지만 특징 1 기준보다는 특징 2 기준이 좀 더 일반적으로 통용되는 정의인 것 같습니다. 일단 mdn docs과 같은  공신력 있는 웹사이트에서는 SPA를 위의 뜻 2에 가깝게 정의하고 설명합니다. mdn에 비해 공신력 있다고 하기는 어렵지만 위키백과에서도 동일하게 설명합니다.

 

 

 

"index.html 하나만 가지기에 SPA다." 말은 주로 개인 블로거들이 포스팅할 때나 개발 커뮤니티 같은 개인의 의견을 나누는 곳에서 사용하는 표현입니다. 더 공식적이고 표준적인 의미는 특징 2지만 특징 1 알고 있는 개발자들도 적지 않다는 것입니다.(저도 그랬구요!) 사실 이전에는 index.html 하나만 갖는 것이 곧 SPA가 맞기도 했습니다.

 

결국 Next.js는 어떤 작동 방식 부문, 사용자 경험 측면에서는 완벽하게 SPA라고 할 수 있지만, Next.js는 기존의 SPA들이 공통적으로 가졌던 특징을 전부 계승하지는 않는 것입니다. 

 

예상 반론

사실 이 글에 대한 가능성이 가장 높은 반박은 "특징 1몇몇 개발자들의 오해일 뿐 뜻 공식적으로 통용되는 의미는 특징 2이다. 그러므로 Next.js는 SPA다."입니다. 사실 이 말은 틀렸다고 하기 어렵습니다. SPA와 MPA 중 어디에 더 부합하는지 둘 중 하나를 무조건 골라야 한다면 SPA가 더 적합할 가능성이 높습니다.

 

그런데 저는 용어의 정의가 맞냐 틀리냐만을 말하고 싶은 게 아닙니다. 실질적으로 통용되는 공식에 가까운 뜻은 특징 2라고 해도 "SPA는 원래 그런 의미 아니니까 특징 2정설인데?" 라고만 결론짓고 특징 1무시하고 고려하지 않는다면 이제부터는 개발상의 예상치 못한 에러를 마주할 수도 있습니다. 

 

Next.js는 전형적인 프레임워크이고 강력한 추상화를 제공합니다. 이는 곧 세부사항을 무시할 수 있다는 것입니다. 그런데 이러한 세부사항을 무시한다면 추상화 아래에서 발생하는 문제는 어떻게 해결할 수 있을까요?

 

이 글을 쓰게 된 이유

바로 이전 포스팅 마지막 단락에 그 이유가 있습니다. 이 단락은 제가 Next.js 앱을 정적 배포하면서 맞닥뜨린 에러에 관한 내용입니다. (사실 이건 예전 프로젝트에서 해결하지 못했던 이슈와 밀접하게 관련이 있는 주제였습니다.)

 

[Next.js] AWS EC2에 Docker와 Node없이 Next.js 정적 배포(Static Exports)하기

(이 글을 Infra와 Next.js 두 개의 카테고리 중 어디에 넣어야 할지 고민했는데 글의 중심 주제는 Next.js의 output: export 설정과 그로 인한 것들이라 Next.js 자체에 좀 더 집중되어 있다고 봐서 Next.js 카

unhandled.tistory.com

 

이 에러는 /project, /project/1 이런 랜딩 페이지 외 경로에 직접 접근 시 해당 페이지를 찾을 수 없던 이슈였습니다. 원인은 리액트 배포 방식으로 작성된 리버스 프록시 때문이었죠. 

 

리액트로 구축된 웹사이트라면 어떤 경로로 진입하든 첫 진입점은 index.html이고 그 안에서 모든 일이 발생합니다. 그러므로 배포 시 nginx 리버스 프록시를 작성할 때 index.html만 찾으라고 하면 됩니다. (정확히 말하면 리액트에서는 모든 페이지 요청에 대해 index.html만 반환(fallback)하면 됩니다.)

 

하지만 Next.js에서는 최초 진입 페이지가 정적 생성된 페이지인 경우 빌드 타임에 생성해 놓은 HTML을 그대로 반환합니다. 이 경우 빌드 산출물의 project.html, 1.html이라는 파일을 찾아서 그 HTML을 반환해야 합니다. 하지만 project.html을 찾는 과정이 이전 리버스 프록시 설정에 없었고, 결국 index.html(랜딩 페이지)을 반환한 것입니다. (주소창으로 직접 접근이 아닌 Link 같은 클라이언트 사이드 내비게이션으로 이동했다면 정상적으로 동작했을 것입니다.)

 

결론

그러므로 Next.js에 대해 "Next.js는 SPA다" 내지는 "Next.js는 MPA다"라고만 정의하고 끝내는 것은 명확하지 않을 수 있습니다. 제가 이 글에서 하고 싶은 말은 "Next.js는 여전히 SPA다"라는 명제가 참이라고 해도

 

"Next.js는 구조적으로는 기존의 SPA와 다른 부분이 있다."

"Next.js는 기존의 SPA와 달리 여러 HTML서버로부터 받을 수 있다."

 

는 것을 알고 있어야 한다는 것입니다.

 

앞서 발생한 문제는 추상화 아래의 문제였습니다. 표준적인 경로(Vercel 배포)를 벗어나는 순간(EC2 배포) 발생하는 추상화 아래의 문제를 파악할 수 없다면 디버깅이 수월하지 않을 것입니다. 

 

마무리

이 모든 것들은 Next.js가 극적인 패러다임의 변화를 다시 한 번 더 가져왔기 때문에 발생한 일입니다. 그리고 Next.js의 등장으로 이런 혼란을 야기하게 된 용어는 SPA 하나만이 아닙니다. (ex) SSR, 렌더링 등)

 

 

+) 진짜 MPA 방식의 프론트엔드 SSR 프레임워크?

더보기

사실 SPA의 특징을 가질 수 있는 MPA 형식의 프론트엔드 SSR 프레임워크라는 표현은 Next.js보다 Astro라는 프레임워크에게 더 적절하다고 합니다. (전 Astro를 사용해 본 경험은 없습니다.) 갑자기 Astro가 튀어나와서 뜬금없어 보일 수 있겠지만.. Astro는 아일랜드 아키텍처라는 방식을 사용하는데 기본적으로 페이지 간 전환 방식이 고전적인 MPA 방식이고, 그중 일부분을 SPA로 구현할 수 있다고 합니다. Next.js는 기본적인 페이지 전환 방식이 SPA이기에 그 맥락이 완전히 다릅니다. 

 

Astro는 공식 문서에서 자신을 MPA(Multi Page Application)이라 소개합니다.

 

 

2번째 문단 해석

이 접근 방식은 Next.js, SvelteKit, Nuxt, Remix 등과 같은 다른 최신 JavaScript 웹 프레임워크와 대조됩니다. 이러한 프레임워크들은 전체 웹사이트의 클라이언트 측 렌더링을 위해 구축되었으며, 주로 성능 문제를 해결하기 위해 서버 측 렌더링을 포함합니다. 이 접근 방식은 Astro의 다중 페이지 앱(MPA) 접근 방식과 대조적으로 단일 페이지 앱(SPA)이라고 불립니다.

 

저 문단에서는 Next.js에 대해서도 "전체 웹사이트의 클라이언트 측 렌더링을 위해 설계되었으며, 주로 성능 문제를 해결하기 위해 서버 측 렌더링을 포함합니다."라고 설명하고 있습니다. 이 부분은 아마 Next.js가 App Router가 아닌 Pages Router만을 사용하고 있을 때 작성된 문항일 것입니다. App Router는 이제 서버 측 렌더링을 기본으로 하기에 저 설명에 부합하지 않습니다.