SPA (Single Page Application)에 대하여
전통적인 웹사이트는 Multi-page Application이다.
매번 요청을 할때마다 깜빡깜빡거려서 눈에 거슬리기는 한다.

모바일 같은 부드러운 경험을 사용자들이 원하기 시작해서
Single Page Application이 유행하기 시작했다 (JS로 가능하긴 하나, 유지보수하기 어려움)
이래서 React같은 SPA 툴들이 성행하기 싲가했다.
반명 React는 Single Page Applicaiton을 만든다
SPA에서는 처음부터 끝까지 JS가 화면을 그린다. (html이 파싱하는 속도보다 JS가 그리는게 더 빠름)
한번만 로드해서 부드럽고 빠르다.



검색엔진 크롤러 (로봇)이 웹사이트를 돌아보며 점수를 매기는데, 기본적으로 html을 보고 채점한다.
React는 index.html 하나만 존재하니 (심지어 안에 내용도 헐렁함)SEO가 좋지 않다.
Next.js는 이거를 보완할 수 있다.
React가 SPA라서 생기는 귀찮은점
- 화면 간 이동(사실상 컴포넌트 교체일 뿐)을 정의하는 것이 불편하다.
- URL이 변하지 않음
- 뒤로가기 / 앞으로가기가 안됨
- 페이지 새로고침하면 초기 페이지로 감
// React는 기본적으로 SPA
function App() {
const [page, setPage] = useState('home');
return (
<div>
<nav>
<button onClick={() => setPage('home')}>홈</button>
<button onClick={() => setPage('about')}>소개</button>
</nav>
{page === 'home' && <Home />}
{page === 'about' && <About />}
</div>
);
}
=> React Router이 이 문제를 해결한다
React Router 만들기
React에서 URL에 따라 다른 컴포넌트(페이지)를 보여주는 라이브러리



npm install react-router
BrowserRouter로 앱 감싸기
React Router을 사용하려면 앱 전체를 <BrowserRouter>로 감싸야한다.
- 브라우저의 URL 감지
- URL 변경을 React에 알림
- 해당 URL에 맞는 컴포넌트를 렌더링
// main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom' // ← 추가
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter> {/* ← App을 감쌈 */}
<App />
</BrowserRouter>
</StrictMode>
)
Routes와 Route

- 사용자가 /about 주소로 이동해서 BroswerRouter이 Routes에 신호 보냄
- Routes가 모든 Route를 순서대로 확인
- path="/about"과 일치하는 ROute 발견
- element={<About/>} 컴포넌트를 화면에 렌더
// App.jsx
import { Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
import Contact from './pages/Contact'
function App() {
return (
<Routes>
// 정확한 경로 (exact match)
<Route path="/" element={<Home />} /> // 주소가 정확히 / 일 때
<Route path="/about" element={<About />} /> // 주소가 정확히 /about 일 때
// 동적 경로 (나중에 배울 내용 미리 보기)
<Route path="/post/:id" element={<Post />} /> // /post/1, /post/2, /post/abc ...
</Routes>
)
}
export default App
정리
- main.jsx에 BrowswerRouter로 전체 감싸기
- app.jsx에 Routes와 Route 정의하기
흔한 실수 모음
- BroswerRouter 없이 ROutes 사용
- element에 컴포넌트를 컴포넌트를 주는게 아니라 문자열로 작성
- Routes없이 Route만 단독 사용
Link로 이동하기
HTML의 기본 링크 태그인 <a>를 그냥 쓰면 SPA의 장점이 사라진다.
- 페이지 전체가 새로 고침됨
- React 앱이 처음부터 다시 로드됨
- 빠르고 부드러운 화면 이동이 안됨
따라서 a태그 대신 React Router의 <Link>를 사용합니다

// components/Navbar.jsx
import { Link } from 'react-router-dom'
function Navbar() {
return (
<nav>
<Link to="/">홈</Link>
<Link to="/about">소개</Link>
<Link to="/contact">연락처</Link>
</nav>
)
}
export default Navbar
Link에는 다른 추가 속성들이 있다
링크에 스타일과 a태그 속성들을 적용할 수도 있다.
// 스타일 적용
<Link to="/about" style={{ color: 'blue' }}>소개</Link>
// className 적용
<Link to="/about" className="nav-link">소개</Link>
// 새 탭에서 열기 (a 태그와 동일)
<Link to="/about" target="_blank">소개</Link>
NavLink로 네비게이션 구현하기
Link와 동일하게 페이지를 이동하지만,
현재 보고 있는 페이지의 링크에 자동으로 active클래스를 추가해준다.
CSS 파일 없이 inline으로도 active 스타일을 줄 수 있다.
active 클래스 외에도 isActive값이 넘어가는데. (현재 페이지면 true, 아니면 false)
이걸로 inline으로 active인지에 따라 동적 스타일을 적용할 수 있다.
inline 말고도 className으로도 동적 스타일을 적용할 수도 있다.


// components/Navbar.jsx
import { NavLink } from 'react-router-dom'
function Navbar() {
return (
<nav>
<NavLink to="/" end>홈</NavLink>
<NavLink to="/about">소개</NavLink>
<NavLink to="/contact">연락처</NavLink>
<NavLink
to="/about"
style={({ isActive }) => ({
color: isActive ? 'blue' : 'black',
fontWeight: isActive ? 'bold' : 'normal',
})}>
소개
</NavLink>
</nav>
)
}
export default Navbar
/* index.css */
/* NavLink가 현재 페이지일 때 자동으로 .active 클래스를 붙여줌 */
a.active {
font-weight: bold;
color: blue;
text-decoration: underline;
}
조금더 예쁘게 작성하려면 다음과 같게 작성하면 좋다

여기서 사용되는 end는 링크가 죄다 /mypage로 되어 있어서
isActive가 orders이나 wishlist일때도 mypage에게도 isActive이 가는데
end가 그걸 막아준다 (독립적이게 막아줌)
// pages/MyPage.jsx — 마이페이지 자체도 레이아웃 역할
import { Outlet, NavLink } from 'react-router-dom'
function MyPage() {
return (
<div>
<h1>마이페이지</h1>
<nav>
<NavLink to="/mypage" end>목록</NavLink>
<NavLink to="/mypage/orders">주문 목록</NavLink>
<NavLink to="/mypage/wishlist">찜 목록</NavLink>
</nav>
<Outlet /> {/* 하위 페이지가 여기에 들어옴 */}
</div>
)
}
export default MyPage
useNavigate로 버튼 클릭 시 페이지 이동하기
클릭 이외의 상황 (로그인 성공, 폼 제출 후 등)에서 페이지를 이동하는 방법입니다.
그리고 위의 태그로 구현하는 방식들은 뒤로가기 기능이 없다 (Link, NavLink)
클릭이라도 뒤로가기 기능이 필요할때도 사용된

import { useNavigate } from 'react-router-dom'
function Home() {
const navigate = useNavigate()
const handleClick = () => {
navigate('/about') // /about 페이지로 이동
}
return (
<div>
<h1>홈 페이지</h1>
<button onClick={handleClick}>소개 페이지로 이동</button>
</div>
)
}
뒤로가기는 navigate(-1)이다.
⭐replace옵션
뒤로가기 불가능하게 만듦
// 기본: 히스토리에 새 항목 추가 (뒤로가기 가능)
navigate('/home')
// replace: 현재 히스토리를 덮어씀 (뒤로가기 불가)
navigate('/home', { replace: true })
replace 사용 케이스
로그인 후 → replace로 이동하면 로그인 페이지로 뒤로가기 불가
navigate('/home', { replace: true })
가급적 상태는 그 상태가 필요한 컴포넌트에 가깝게 두어야한다.
Outlet
중첩 라우팅할때 사용된다.
Outlet은 자식 페이지가 여기에 들어온다는 표시이다.
공통 레이아웃은 부모 컴포넌트에 한번만 쓰고,
바뀌는 내용만 Outlet 자리에 들어온다.
// layouts/MainLayout.jsx
import { Outlet } from 'react-router-dom'
import Header from '../components/Header'
import Sidebar from '../components/Sidebar'
function MainLayout() {
return (
<div>
<Header /> {/* 모든 페이지 공통 헤더 */}
<Sidebar /> {/* 모든 페이지 공통 사이드바 */}
<Outlet /> {/* ← 자식 페이지 내용이 여기 들어옴 */}
</div>
)
}
export default MainLayout


/about하면 /의 layout과 about이 같이 보인다
알아야하는 것은, /about은 절대경로라서 사용하면 안되고
about만 써야지 상대경로로 파악되어서 Layout 역할을 한다.
하위 메뉴(2단계 중첩)를 만들때에도 Outlet이 사용된다.
이 중 기본 페이지 설정은 index 로 표시한다.
// App.jsx — 2단계 중첩
function App() {
return (
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
{/* MyPage 하위에 또 중첩 */}
<Route path="/mypage" element={<MyPage />}>
<Route index element={<MyPageMain />} />
<Route path="orders" element={<Orders />} />
<Route path="wishlist" element={<Wishlist />} />
</Route>
</Route>
</Routes>
)
}
useParams로 동적 라우팅하기
//id params로 전달하기
function Products() {
return (
<div>
<h1>상품 목록</h1>
{productList.map((product) => (
<div key={product.id}>
<Link to={`/products/${product.id}`}>
{product.name}
</Link>
</div>
))}
</div>
)
}
// params에 id 가져오기
import { useParams } from 'react-router-dom'
function ProductDetail() {
const { id } = useParams() // URL의 :id 값을 가져옴
return (
<div>
<h1>{id}번 상품 상세페이지</h1>
</div>
)
}
export default ProductDetail
// ✅ 동적 라우팅으로 한 줄로 해결
<Route path="/products/:id" element={<ProductDetail />} />
// :id 자리에 어떤 값이 와도 ProductDetail 컴포넌트를 보여줌
// /products/1 → ProductDetail (id = "1")
// /products/2 → ProductDetail (id = "2")
// /products/abc → ProductDetail (id = "abc")
이때 뒤의 id값은 useParams로 찾아준다. 리턴값은 항상 string이다.

(+) 만약 여러개면 어떻게 하는가?

useParams에서 가져오는 데이터 이름은 Routes/Route에서 지정해주는 그대로 가져온다 (:id -> id)
404 페이지 처리하기
존재하지 않는 URL로 접근했을때 404페이지로 redirect 해줘야합니다.
이는 Routes / Route로 처리하며, path="*"로 하고 element를 404페이지 컴포넌트를 걸어주면
위에 정의한 Route중 아무것도 일치하지 않을때 404 페이지로 이동해줍니다.
이는 반드시 Routes 안에 가장 마지막에 작성해야합니다
// App.jsx
import NotFound from './pages/NotFound'
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
{/* 위의 Route 중 아무것도 일치하지 않으면 여기로 */}
<Route path="*" element={<NotFound />} />
</Routes>
)
}
Navigate로 자동 리다이렉트
조건에 따라 자동으로 다른 페이지로 보냅니다.
Navigate는 렌더링되는 순간 즉시 다른 페이지로 이동시키는 컴포넌트다.
useNavigate가 함수로 이동시킨다면, <Navigate/>는 JSX로 이동시킨다.
// pages/MyPage.jsx
import { Navigate } from 'react-router-dom'
function MyPage() {
const isLoggedIn = false // 로그인 상태 (나중에 실제 로그인 로직으로 교체)
// 로그인 안 했으면 로그인 페이지로 보내기
if (!isLoggedIn) {
return <Navigate to="/login" />
}
return (
<div>
<h1>마이페이지</h1>
<p>로그인한 사용자만 볼 수 있습니다.</p>
</div>
)
}
export default MyPage

로그인 리다이렉트에는 보통 replace를 사용한다. ⭐
로그인 페이지에서 뒤로가기를 눌러 마이페이지 (권한없는 페이지)로 돌아오는 것을 막기 위해서이다.
// replace 없음: 히스토리에 /login 이 추가됨 → 뒤로가기 시 /mypage 로 돌아옴
<Navigate to="/login" />
// replace 있음: /mypage 히스토리를 /login 으로 덮어씀 → 뒤로가기 불가
<Navigate to="/login" replace />
ProtectedRoute
로그인이 필요한 페이지들을 한번에 보호하는 패턴이다.
// components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom'
function ProtectedRoute({ children }) {
const isLoggedIn = false // 실제 로그인 상태로 교체
if (!isLoggedIn) {
return <Navigate to="/login" replace />
}
return children // 로그인 됐으면 원래 페이지 표시
}
export default ProtectedRoute
// App.jsx
import ProtectedRoute from './components/ProtectedRoute'
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* 로그인이 필요한 페이지들을 ProtectedRoute로 감싸기 */}
<Route
path="/mypage"
element={
<ProtectedRoute>
<MyPage />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
)
}

useSearchParams로 쿼리스트링 다루기
쿼리스트링 형태의 값을 읽고 쓸때 사용하는 훅이다.
import { useSearchParams } from 'react-router-dom'
function Products() {
const [searchParams, setSearchParams] = useSearchParams()
// URL에서 값 읽기
const category = searchParams.get('category') // "laptop" or null
const sort = searchParams.get('sort') // "price" or null
return (
<div>
<h1>상품 목록</h1>
<p>카테고리: {category ?? '전체'}</p>
<p>정렬: {sort ?? '기본'}</p>
</div>
)
}
쿼리스트링 값을 변경하고 싶으면 다음과 같이 해야한다.
다만, 덮어씌우는 방식이기 때문에
만약 쿼리가 여러개였는데 객체 하나만 넣으면 기존의 다른 쿼리스트링 값들이 사라진다.
다만, hook이기 때문에 리렌더링이 일어난다.
useSearchParams() 실행한 페이지는 query string이 변경되면 리렌더링된다.
⭐hook은 보통 리렌더링과 관련된 함수이다. (상태가 바뀌면 리렌더링된다)
function Products() {
const [searchParams, setSearchParams] = useSearchParams()
const handleCategoryChange = (category) => {
setSearchParams({ category })
// URL이 /products?category=laptop 으로 변경됨
}
const handleSortChange = (sort) => {
setSearchParams({
category: searchParams.get('category'), // 기존 category 유지
sort,
})
}
return (
<div>
<h1>상품 목록</h1>
<div>
<button onClick={() => handleCategoryChange('laptop')}>노트북</button>
<button onClick={() => handleCategoryChange('phone')}>스마트폰</button>
<button onClick={() => handleCategoryChange('audio')}>음향기기</button>
</div>
<div>
<button onClick={() => handleSortChange('price')}>가격순</button>
<button onClick={() => handleSortChange('newest')}>최신순</button>
</div>
</div>
)
}
useLocation으로 현재 위치 알아내기
- pathname: 경로
- search: 쿼리스트링 (문자열 그대로)
- state: navigate로 전달한 데이
import { useLocation } from 'react-router-dom'
function SomeComponent() {
const location = useLocation()
console.log(location)
}
// URL: /products?category=laptop#section1 일 때
{
pathname: "/products", // 경로
search: "?category=laptop", // 쿼리스트링 (문자열 그대로)
hash: "#section1", // 해시
state: null, // navigate()로 전달한 데이터
key: "default" // 고유 키
}
또한, navigate()로 state를 전달할때 useLocation으로 받는다
페이지 이동 시 데이터를 함꼐 넘길 수 있다. URL에는 표시되지 않는다.
아주 가끔 사용되나, 그렇게 권장하지는 않는다
유지보수하기 어렵기 때문이다.
이거 쓸 바에 searchParams를 쓰는 것이 좋다


Vite React Project 배포하기
npm run dev해서 dev환경에서는 잘 돌아간거 확인했으면 이제 배포할 준비를 할 차례다.
1. npm run build를 한다
2. npm run preview를 한다.
preview 봐도 괜찮으면 git repository에 연결해서
그걸 vercel에 던져주면 된다.
문제가 배포를 하면 새로고침이 안된다는 점이다.


이런 문제 때문 vercel.json을 적용해줘야한다.
"어떤 URL이 와도(/*) index.html 을 돌려줘(200)" → React Router가 URL을 받아서 알맞은 컴포넌트를 렌더링
// vercel.json (프로젝트 루트에 생성)
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
https://github.com/quothraven1122/react-vite-deploy-practice
https://react-vite-deploy-practice.vercel.app/
정리
1. SPA의 개념
2. React Router
- BroswerRouter
- Routes
-Route
- Outlet (중첩 라우팅)
- Link
- NavLink
- Navigate - replace옵
- useNavigate
- useParams (URL 값 읽기 ex. :id)
- useSearchParams (쿼리스트링)
- useLocation (현재위치)
3. ProtectedRoute 패턴
4. 404 패턴
'코드잇 스터디' 카테고리의 다른 글
| [관계형DB] 데이터베이스를 활용한 JS (0) | 2026.05.11 |
|---|---|
| [Express] JS로 백엔드 개발 시작하기 (0) | 2026.04.29 |
| [React] React로 데이터 다루기 (1) | 2026.04.22 |
| [React] 리엑트에 대하여 (0) | 2026.04.21 |
| [JS 기초문법] 리퀘스트 보내기 (0) | 2026.04.14 |