
SPA vs MPA: 새로고침과의 전쟁
웹이 앱처럼 부드러워진 비결(SPA)과 옛날 방식(MPA)의 장단점. 그리고 이 둘을 합친 넥스트(Next.js)의 등장.

웹이 앱처럼 부드러워진 비결(SPA)과 옛날 방식(MPA)의 장단점. 그리고 이 둘을 합친 넥스트(Next.js)의 등장.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

2010년, 네이버 뉴스를 볼 때는 늘 이랬습니다.
기사 클릭 → 화면 하얗게 깜빡 → 로딩 → 새 페이지
저는 답답했습니다. "왜 매번 화면을 새로 그려야 해?"
그런데 2024년, 페이스북이나 트위터를 보면:
포스트 클릭 → 부드럽게 전환 → 깜빡임 없음
이게 너무 신기했습니다. 둘 다 웹사이트인데, 하나는 책장 넘기듯 느리고, 하나는 앱처럼 빠릅니다. 도대체 뭐가 다른 건지 궁금했습니다.
리액트로 첫 프로젝트를 만들었을 때였습니다.
개발: 완벽 ✅
배포: 완벽 ✅
구글 검색: 안 나옴 ❌
구글에 제 사이트가 아예 안 잡히더군요. 그리고 첫 화면이 뜰 때까지 5초나 걸렸습니다. 사용자가 뒤로가기를 누르면 이상한 동작도 했습니다.
시니어 개발자가 한마디 했습니다.
"SPA랑 MPA 차이를 모르니까 그래. Next.js로 마이그레이션해."
솔직히 무슨 말인지 몰랐습니다. SPA? MPA? Single Page Application? 그게 뭔데요?
그때부터 웹 렌더링 방식을 정리해본다는 생각으로 공부를 시작했습니다.
처음 SPA 설명을 읽었을 때는 전혀 와닿지 않았습니다.
"SPA는 하나의 HTML 파일로 여러 페이지를 구현하는 방식입니다."
이 문장을 10번 읽어도 이해가 안 됐습니다. 하나의 HTML인데 어떻게 여러 페이지가 돼요? 그럼 URL은 어떻게 바뀌는 건데?
그리고 "CSR", "SSR", "SSG" 같은 약자들이 쏟아져 나왔습니다. 하나도 모르겠더군요.
가장 답답했던 건:
"결국 페이스북은 어떻게 구현된 건데?"
그냥 이것만 알고 싶었습니다.
시니어 개발자가 비유를 하나 들어줬습니다. 그 순간 머릿속이 확 정리됐습니다.
책을 읽는다고 생각해봐.
1장 → 2장 넘어갈 때:
- 책을 완전히 닫음
- 다시 펼침
- 표지(헤더)도 다시 보임
- 목차(네비게이션)도 다시 로딩
→ 매번 "완전히 새로운 책"을 펼치는 느낌
프레젠테이션 슬라이드를 넘긴다고 생각해봐.
슬라이드 1 → 2 넘어갈 때:
- 상단 헤더는 그대로
- 내용만 쓱 바뀜
- 부드러운 전환 효과
- 하지만 슬라이드 파일 전체를 미리 다운받아야 함
→ "하나의 파일 안에서 화면만 바꾸는 느낌"
이 비유가 와닿았습니다. "아, 페이지를 통으로 바꾸느냐(MPA) vs 내용만 바꾸느냐(SPA)의 차이구나!"
결국 이거였다는 걸 받아들였습니다. 페이스북이 빠른 이유는 서버에서 HTML을 새로 받지 않고, 브라우저가 JavaScript로 화면만 바꾸기 때문이었습니다.
전통적인 웹사이트 방식입니다. 워드프레스, 네이버 블로그 같은 곳이 이렇게 동작합니다.
사용자: "홈" 클릭
1. 브라우저: "서버야, home.html 파일 줘"
2. 서버: "여기 있어" (HTML 전송)
3. 브라우저: 화면 전체 새로 그림 (깜빡)
사용자: "블로그" 클릭
4. 브라우저: "서버야, blog.html 파일 줘"
5. 서버: "여기 있어" (HTML 전송)
6. 브라우저: 화면 전체 새로 그림 (또 깜빡)
→ 매번 새로고침 (하얀 화면 플래시)
<!-- home.html -->
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<h1>Welcome!</h1>
</main>
</body>
</html>
<!-- blog.html -->
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
<link rel="stylesheet" href="style.css"> <!-- ← 또 다운! -->
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav> <!-- ← 헤더도 또 그림! -->
</header>
<main>
<h1>Blog Posts</h1>
</main>
</body>
</html>
파일이 완전히 분리되어 있습니다. 페이지마다 HTML이 다릅니다. 브라우저는 매번 서버에서 새 HTML을 받아야 합니다.
1. SEO 최강:
- 구글 로봇이 HTML을 바로 읽음
- 검색 순위 잘 나옴
- 크롤링 쉬움
2. 구현 간단:
- HTML 파일만 만들면 됨
- JavaScript 거의 필요 없음
- PHP/Django 같은 백엔드만 있으면 됨
3. 초기 로딩 빠름:
- 필요한 페이지만 받으니까
- 불필요한 코드 안 받음
이 단점들이 저한테는 너무 와닿았습니다. 제가 2010년에 느낀 답답함이 바로 이것들이었습니다.
1. 깜빡임:
- 페이지 전환마다 화면 새로고침
- UX 구림
- 모던한 느낌 안 남
2. 중복 전송:
- 헤더, 푸터를 매번 다시 다운
- CSS, JavaScript도 매번 재요청 (캐싱 제외)
- 네트워크 낭비
3. 느림:
- 매번 서버 왕복 (RTT)
- 서버에서 HTML 생성 시간도 걸림
- 사용자는 하얀 화면을 계속 봄
드디어 이해했다고 느낀 부분입니다.
사용자: 처음 접속
1. 브라우저: "서버야, 모든 코드 다 줘"
2. 서버: "여기 있어" (index.html + 거대한 bundle.js)
3. 브라우저: JavaScript 실행
4. React/Vue 앱이 시작됨
사용자: "홈" → "블로그" 클릭
1. JavaScript: URL을 변경 (history.pushState)
2. JavaScript: 화면 내용만 쓱 변경
3. 서버 요청 없음!
4. 깜빡임 없음!
사용자: 데이터 필요
1. JavaScript: "서버야, JSON만 줘" (fetch/axios)
2. 서버: "{ posts: [...] }"
3. JavaScript: JSON으로 화면만 업데이트
핵심은 "처음에 JavaScript를 다 받고, 이후엔 브라우저가 알아서 화면을 바꾼다"는 겁니다.
// App.jsx (React)
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog" element={<Blog />} />
<Route path="/about" element={<About />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
function Layout({ children }) {
return (
<>
<header>
<nav>
<Link to="/">Home</Link> {/* ← 클릭해도 새로고침 안 됨! */}
<Link to="/blog">Blog</Link>
<Link to="/about">About</Link>
</nav>
</header>
<main>
{children} {/* ← 여기만 바뀜 */}
</main>
</>
);
}
function Blog() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// AJAX로 데이터만 가져옴
fetch('/api/posts')
.then(res => res.json())
.then(setPosts);
}, []);
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.summary}</p>
</article>
))}
</div>
);
}
보시면 HTML 파일은 하나(index.html)입니다. 그 안에서 JavaScript가 페이지를 바꿉니다. <Link>를 클릭하면 React Router가 URL만 바꾸고 화면만 업데이트합니다.
1. UX 최강:
- 깜빡임 없음
- 앱처럼 부드러움
- 페이지 전환 애니메이션 가능
- 페이스북, 트위터가 이래서 빠름
2. 빠른 페이지 전환:
- 서버 요청 없이 화면만 바꿈
- 데이터만 JSON으로 받음 (HTML보다 작음)
- 즉각 반응
3. 상태 유지:
- 페이지 이동해도 JavaScript 변수 그대로
- 음악 재생 중에도 페이지 전환 가능
- YouTube 같은 경우 영상 계속 재생됨
이 부분이 제가 삽질한 이유였습니다.
1. 초기 로딩 느림:
- 모든 JavaScript를 다운 (bundle.js가 몇 MB)
- 첫 방문이 느림
- 사용자는 "빈 화면"을 오래 봄
2. SEO 문제:
- 구글 로봇이 빈 HTML만 봄
- <div id="root"></div> ← 내용이 없음!
- JavaScript 실행 후에야 내용이 나타남
- 구글이 JavaScript를 실행할 수는 있지만 느림
3. JavaScript 필수:
- JS 꺼지면 아예 빈 화면
- JavaScript 에러 나면 전체 앱이 죽음
제가 겪은 실제 문제입니다.
React로 블로그를 만듦:
- 개발 환경: 완벽 ✅
- 프로덕션 배포: 완벽 ✅
- 구글 검색: 3주가 지나도 안 잡힘 ❌
이유:
구글 로봇이 본 것:
<html>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
→ 내용이 아예 없음!
→ 구글: "이 사이트는 비어있네? 인덱싱 안 할래."
제 블로그에는 20개 글이 있었는데, 구글 입장에선 아무것도 없는 빈 사이트였습니다. 이게 너무 답답했습니다.
Next.js로 이전:
- 서버가 미리 HTML을 생성 (SSR/SSG)
- 구글 로봇이 완성된 HTML을 봄
- 3일 만에 구글 검색에 잡힘
- 검색 순위도 올라감
이때 받아들였습니다. "SPA는 사용자 경험은 좋은데, SEO는 안 좋구나. 둘 다 잡으려면 Next.js 같은 프레임워크가 필요하구나."
여기가 진짜 핵심이었습니다. 이 개념을 이해했다고 느낀 순간, 모든 게 연결됐습니다.
"첫 페이지는 MPA처럼, 이후 이동은 SPA처럼"
1. 첫 방문:
- 서버가 완성된 HTML을 보냄 (MPA 방식)
- 구글 로봇: "오, HTML 있네!" ✅
- 사용자: "화면 빠르네!" ✅
2. 이후 페이지 이동:
- JavaScript로 화면만 변경 (SPA 방식)
- 깜빡임 없음 ✅
- 부드러움 ✅
→ MPA의 장점(SEO, 빠른 첫 로딩) + SPA의 장점(부드러움)을 둘 다 가져감!
이 방식이 정말 똑똑하다고 생각했습니다. "왜 진작에 이걸 안 만들었을까?" 싶었는데, 알고 보니 기술적으로 복잡해서 2016년에야 Next.js가 나왔더군요.
// pages/index.js (홈 페이지)
export default function Home({ posts }) {
return (
<div>
<h1>Latest Posts</h1>
{posts.map(post => (
<Link key={post.id} href={`/post/${post.slug}`}>
<article>
<h2>{post.title}</h2>
<p>{post.summary}</p>
</article>
</Link>
))}
</div>
);
}
// 이 함수는 서버에서만 실행됨 (SSR)
export async function getServerSideProps() {
// 데이터베이스에서 글 목록 가져오기
const posts = await db.getPosts();
return {
props: { posts } // ← 이 데이터가 HTML에 포함되어 전송됨
};
}
사용자가 / 페이지에 접속하면:
getServerSideProps 실행posts 가져옴구글 로봇이 보는 것:
<div>
<h1>Latest Posts</h1>
<article>
<h2>첫 번째 글 제목</h2>
<p>요약...</p>
</article>
<article>
<h2>두 번째 글 제목</h2>
<p>요약...</p>
</article>
</div>
내용이 다 있습니다! SEO 완벽!
Next.js의 마법 같은 개념입니다. 처음엔 이해가 안 됐는데, 비유를 듣고 나서 와닿았습니다.
1. 서버: HTML 생성 (뼈대)
<div>
<h1>Latest Posts</h1>
<button>Like</button> ← 클릭 안 됨 (정적 HTML)
</div>
2. 브라우저: HTML 즉시 표시 (빠름!)
- 사용자는 이미 화면을 봄
- 하지만 버튼 클릭 안 됨
3. JavaScript 다운로드 & 실행
4. React가 HTML에 "생명을 불어넣음" (Hydration)
- 버튼에 이벤트 리스너 부착
- 이제 클릭 가능!
5. 이후 링크 클릭:
→ 페이지 새로고침 없이 전환 (SPA 방식)
"Hydration"이라는 단어가 와닿았습니다. 마른 스펀지(정적 HTML)에 물을 부어서(JavaScript) 살아있는 앱으로 만드는 거죠.
이 세 개념을 정리해본다면:
전통적인 SPA 방식 (Create React App):
1. 브라우저가 빈 HTML + bundle.js 받음
2. JavaScript가 실행됨
3. 화면 렌더링
장점: 서버 부하 없음
단점: 첫 로딩 느림, SEO 약함
예: 관리자 대시보드, 내부 도구
Next.js 동적 방식:
1. 요청마다 서버가 HTML 생성
2. 브라우저가 완성된 HTML 받음
3. JavaScript 로드 → Hydration
장점: 빠른 첫 로딩, SEO 좋음
단점: 서버 부하 높음
예: 뉴스 사이트, SNS 타임라인
Next.js 코드:
export async function getServerSideProps() { ... }
빌드 타임에 생성:
1. 빌드할 때 HTML을 미리 만듦
2. CDN에 올림
3. 요청 시 즉시 HTML 전송 (초고속!)
장점: 가장 빠름, 서버 부하 없음, SEO 완벽
단점: 데이터 변경 시 재빌드 필요
예: 블로그, 문서, 마케팅 페이지
Next.js 코드:
export async function getStaticProps() { ... }
| 방식 | 첫 로딩 속도 | SEO | 서버 부하 | 실시간 데이터 | 적합한 경우 |
|---|---|---|---|---|---|
| CSR | 느림 (3-5초) | 약함 ❌ | 없음 | 가능 ✅ | 대시보드, 관리자 페이지 |
| SSR | 빠름 (0.5-1초) | 강함 ✅ | 높음 ⚠️ | 가능 ✅ | 뉴스, SNS, 전자상거래 |
| SSG | 초고속 (0.1초) | 강함 ✅ | 없음 ✅ | 불가능 ❌ | 블로그, 문서, 포트폴리오 |
이 부분은 제가 실제로 프로젝트에 적용한 내용입니다.
// 1. 블로그 포스트: SSG (내용이 거의 안 바뀜)
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return {
props: { post },
revalidate: 3600 // 1시간마다 재생성 (ISR)
};
}
// 2. 실시간 주식 차트: SSR (자주 바뀜)
export async function getServerSideProps() {
const stock = await fetchStockPrice();
return {
props: { stock }
};
}
// 3. 사용자 대시보드: CSR (개인정보, SEO 불필요)
export default function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/user/dashboard')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <Loading />;
return <div>{data.name}님의 대시보드</div>;
}
이 전략을 이해했다면, 99%의 웹 프로젝트는 해결됩니다.
제 블로그를 마이그레이션했을 때 실제 측정한 수치입니다.
첫 로딩 시간: 4.2초
구글 검색: 3주 지나도 안 잡힘
Lighthouse 점수: 42점
FCP (First Contentful Paint): 3.8초
TTI (Time to Interactive): 5.1초
사용자 반응: "느려요"
첫 로딩 시간: 0.8초 (↓ 80%)
구글 검색: 3일 만에 상위 노출
Lighthouse 점수: 98점
FCP: 0.4초
TTI: 1.2초
사용자 반응: "빨라졌네요!"
이 수치를 보고 나서 확실히 받아들였습니다. "Next.js는 선택이 아니라 필수구나."
Next.js 배우고 신나서:
- 블로그 글: SSR
- About 페이지: SSR
- Contact 페이지: SSR
결과:
- 서버 CPU 100%
- 오히려 느려짐
- 서버 비용 폭증
깨달음:
→ 정적 페이지는 SSG를 써야 함
→ SSR은 진짜 동적인 것만
// 서버에서 렌더링
<div>현재 시간: {new Date().toString()}</div>
// 클라이언트에서 Hydration
<div>현재 시간: {new Date().toString()}</div>
→ 시간이 달라서 에러!
Warning: Text content did not match.
Server: "2024-02-04 17:00:00"
Client: "2024-02-04 17:00:01"
처음에 이 에러가 왜 나는지 몰랐습니다. 알고 보니 서버와 클라이언트의 렌더링 결과가 달라서 그랬습니다.
해결 방법:
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div>Loading...</div>;
}
return <div>현재 시간: {new Date().toString()}</div>;
서버에선 "Loading..." 보여주고, 클라이언트에서만 실제 시간 보여주기.
CRA로 만든 SPA:
bundle.js: 5.2MB 😱
→ 모바일에서 첫 로딩 15초
→ 사용자 이탈률 80%
원인:
- moment.js (거대한 라이브러리) 전체 임포트
- lodash 전체 임포트
- 안 쓰는 컴포넌트도 번들에 포함
해결:
1. Code Splitting: React.lazy()
2. Tree Shaking: import { format } from 'date-fns'
3. Bundle Analyzer로 크기 분석
4. moment.js → date-fns (더 가벼움)
결과:
bundle.js: 500KB (↓ 90%)
첫 로딩: 3초
Next.js가 승리했다고 생각했는데, 또 새로운 전쟁이 시작됐습니다.
현재 Next.js는 Hydration 때 JavaScript를 많이 보냅니다.
문제:
- 헤더(정적): JavaScript 불필요한데 Hydrate됨
- 푸터(정적): JavaScript 불필요한데 Hydrate됨
- 캐러셀(인터랙티브): JavaScript 필요
→ 전부 Hydrate하니까 느림
Astro의 아이디어:
"필요한 부분만 Hydrate하자"
- 헤더: 정적 HTML (0 JS)
- 푸터: 정적 HTML (0 JS)
- 캐러셀: React Island (여기만 Hydrate)
→ JavaScript 크기 ↓ 90%
이게 Islands Architecture입니다. MPA에 작은 SPA 섬들을 띄운 느낌입니다.
Hydration은 모든 컴포넌트를 한 번 실행해서 이벤트 리스너를 붙입니다.
Qwik의 아이디어:
"아예 실행하지 말자"
- 이벤트 리스너를 HTML에 직렬화
- JavaScript는 클릭할 때만 실행
- First Load JS = 0kb
→ 즉시 인터랙티브
이 기술을 이해했다는 느낌이 들었을 때, "웹은 계속 진화하는구나"라고 받아들였습니다.
✅ 관리자 대시보드
✅ 내부 도구 (Jira, Notion 같은)
✅ SEO 완전 불필요
✅ 인증이 필수인 페이지
✅ 빠른 프로토타입
✅ 블로그
✅ 뉴스 사이트
✅ 전자상거래 (쿠팡, 아마존)
✅ SNS (트위터, 인스타)
✅ SEO가 중요한 모든 것
✅ 첫 로딩이 중요한 모든 것
→ 2025년 기준 95%의 프로젝트는 Next.js가 정답
✅ 정적 블로그 (인터랙션 거의 없음)
✅ 문서 사이트 (Docs)
✅ 마케팅 페이지
✅ 포트폴리오
→ Next.js보다 더 빠름 (JavaScript 거의 없음)
✅ 간단한 블로그 (비개발자가 관리)
✅ JavaScript 몰라도 됨
✅ 플러그인으로 해결 가능
✅ 레거시 유지보수
→ 2025년엔 거의 안 씀
처음에 던진 질문을 돌이켜봅니다.
"왜 페이스북은 안 깜빡여?"
답은 User Experience 때문이었습니다. 사용자는 기다리는 걸 싫어합니다. 하얀 화면을 보는 걸 싫어합니다.
SPA(그리고 하이브리드)가 이 문제를 해결했습니다.
이제 우리는 "웹페이지"를 만드는 게 아닙니다. "브라우저에서 돌아가는 앱"을 만듭니다.
2025년에 새 프로젝트를 시작한다면:
망설이지 말고, Next.js를 기본으로 깔고 시작하세요. 95%의 상황에서 완벽하게 작동합니다.
결국 이거였다는 걸 정리해본다면: "웹의 미래는 '빠르고 부드러운 경험'이다."