091
[JS/React] 리액트 라우터(React Router) 본문
1. SPA
- SPA(Single Page Application)란 하나의 HTML 페이지에서 필요한 데이터만 가져오는 형태로, URL 별로 여러개의 html 파일로 구성되며 새롭게 로드되는 MPA(Multi Page Application)와 반대되는 개념입니다.
SSR, CSR, SPA
1. SSR(Server Side Rendering)&CSR(Client Side Rendering)+SPA - SSR은 브라우저가 특정 URL을 요청하면 서버가 데이터베이스를 조회하여 완성된 형태의 HTML문서를 동적으로 생성한 후 클라이언트에 응답하는 전
in-ouput91.tistory.com
-> SPA는 웹 애플리케이션에서 단일 HTML 페이지로 동작하는 애플리케이션 구조로, 페이지를 이동할 때 새로운 HTML 파일로 로드하여 이동하지 않고, 동적으로 필요한 데이터만 가져와 컨텐츠를 업데이트를 합니다. 그러기 때문에 클라이언트 중심의 CSR(Client Side Rendering)을 통해 필요 데이터만 비동기로 가져와 UI를 업데이트합니다.
사용자가 /about 클릭 -> History API로 URL만 /about으로 변경 -> Routes가 변경된 URL 감지
-> path="/about"에 매칭된 <About /> 렌더링 == 서버 요청 없음
-> History API는 HTML5에서 추가된 브라우저 내장 기능으로, 서버에 요청을 보내지 않고 URL과 히스토리만 바꿀 수 있습니다. 이 API는 만약 해당 URL로 이동 후에 새로고침을 시도하면 서버로 GET /about 요청이 가기 때문에 404 에러가 발생할 수 있고 이를 해결하기 위해 React Router가 현재 URL을 읽어 About 컴포넌트를 렌더링 해야합니다. 아래의 두 메서드는 History API의 핵심 메서드입니다.
// pushState: 새 URL을 히스토리에 추가
// → 뒤로가기 하면 이전 URL로 돌아갈 수 있음
history.pushState(null, '', '/about');
// replaceState: 현재 URL을 교체
// → 히스토리에 안 쌓임, 뒤로가기 불가
history.replaceState(null, '', '/about');
[JS/React] JSX(1): 개념과 초기 세팅 설명
1. JSX- JSX란 JavaScript와 HTML/XML을 한 파일에서 쓸 수 있도록 한 React 전용 확장 문법입니다. JSX를 사용하면 createElement(...)와 같은 복잡한 방법 대신 HTML과 비슷하게 생긴 문법을 사용할 수 있어 훨씬
in-ouput91.tistory.com
->아래 글에서 초기 세팅에 생기는 index.html 하나인 앱이 만들어지며 이게 React에서의 SPA 구조의 기본입니다.
-> SPA는 최조 로딩 이후 페이지 로드 없이 데이터만 요청하므로 빠른 로딩 속도, 더 나은 사용자 경험(UX), 서버 부하 감소 등의 장점이 있지만, SEO 문제나 초기 로딩 시간(JS 번들에 따라 로딩 시간 증가)이 증가하는 등의 문제도 가집니다.
- 라우팅(Routing)이란 URL 경로에 따라 사용자가 보게 될 컨텐츠를 결정하는 방식으로, SPA의 경우, 라우팅이 클라이언트 측에서 수행되며, 브라우저의 URL은 변경되지만 페이지는 다시 로드되지 않습니다.
-> React에서는 라우팅을 위해 React Router 라이브러리(react-router-dom)을 사용합니다.(npm install react-router-dom)
-> React Router의 핵심 컴포넌트는 BrowserRouter, Routes, Route, Link 총 4가지입니다.
<BrowserRouter> // 라우팅 기능 전체 앱에 적용
<NavBar /> // Routes 밖에 있으면 모든 페이지에 표시
<Routes> // URL 매칭 필터
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<Error />} />
</Routes>
<Footer /> // 이것도 모든 페이지에 표시
</BrowserRouter>
• BrowserRouter는 라우팅 기능을 전체 앱에 적용하는 최상위 컴포넌트입니다.
• Routes는 여러 라우트를 감싸는 컨테이너 역할의 컴포넌트이고 Route는 URL 경로와 해당 경로에 매칭되는 컴포넌트를 정의합니다. Routes 없이 Route만 사용하면 URL이 뭐인지와 상관없이 있는 모든 Route 컴포넌트를 렌더링하려고 시도합니다. 즉 필터링을 해주지 않기 때문에 Routes를 작성해줘야합니다.
• Link는 페이지 이동을 위한 내비게이션 링크를 생성합니다. <a> 태그와 비슷하게 생겼지만 동장 방식이 다릅니다. 클릭 시 서버에 요청하여 페이지 전체를 새로고침하는 것이 아닌 클릭시 History API 호출과 컴포넌트 렌더링이 일어납니다. 추가로 만약에 현재 페이지를 강조해야할 때는 NavLink를 사용해주는 것이 좋습니다. NavLink는 현재 URL과 일치하면 활성화 상태(isActive)을 알려주기 때문에 색이나 볼드체 등을 설정할 수 있습니다.
+) 위에서 언급한 History API의 한계인 새로고침을 해결할 수 있는 React Router 컴포넌트는 BrowerRouter와 HashRouter가 있습니다. ( Router는 가장 기본 베이스 수준의 React Router이며, 두 라우터 외에도 MemoryRouter라는 컴포넌트도 있습니다.)
| Router 종류 | BrowerRouter | HashRouter |
| URL | example.com/about | example/#/about |
| 새로 고침 시 서버 요청 | GET /about | GET /(# 뒤는 전송이 안됨) |
| 서버가 받는 것 | /about(about.html 없으면 404) | /(index.html로 설정없이 이동) |
| 서버 설정 필요성 | O | X |
| URL 깔끔성 | O | X |
| SEO | 상대적으로 유리 | 상대적으로 불리 |
- 동적 라우팅은 URL의 일부를 변수처럼 사용하여 여러 데이터를 처리하는 라우팅 방식으로, 아래 URL 예시와 같이 상품 ID에 따라 다른 페이지를 보여줄 수 있습니다. 컴포넌트에서 파라미터를 가져와 사용할 때는 useParams 훅 함수를 사용하여 가져와줍니다.
// Route 정의
<Route path="/product/:category/:id" element={<Product />} />
// 컴포넌트에서 사용
function Product() {
const { category, id } = useParams();
// /product/books/101
// category = "books", id = "101"
}
-> :(콜론)으로 이 자리에 어떤 값이 올 수 있고 그 값을 id라는 이름으로 저장하여 사용할 수 있음을 나타내는 것입니다.
-> url에서 지정한 파라미터를 이용할 때는 state함수를 사용했던 것처럼 id, category를 꺼내서 컴포넌트에서 사용할 수 있습니다. 주의할 점은 useParams로 가져온 값은 항상 문자열 형식으로 반환되기 때문에 숫자를 비교할 때는 추가적인 작업을 필요로 합니다.
- 실습: 상품 상세 페이지
import { useParams } from 'react-router-dom';
const productData = {
'books': {
'101': {
name: '리액트 입문',
price: 18000
},
'102': {
name: '자바스크립트 입문',
price: 22000
}
},
'electronics': {
'201': {
name: '마우스',
price: 25000
},
'202': {
name: '키보드',
price: 55000
}
}
};
function ProductDetail() {
const { category, id } = useParams();
const categoryData = productData[category];
const product = categoryData ? categoryData[id] : null;;
return (
<div>
<h2> 상품 상세 정보</h2>
<p> 카테고리: {category}</p>
<p>상품 ID: {id}</p>
{product ? (
<div>
<p>{product.name}</p>
<p>{product.price}원</p>
</div>
) : (
<p style={{ color: 'red' }}>상품을 찾을 수 없습니다</p>
)}
</div>
)
}
export default ProductDetail;
-> 들어온 파라미터가 ggi처럼 productData에 없는 키로 접근하려고 하면 categoryData는 undefined가 되어 null이 들어가 결과적으로 1번 이미지와 같은 결과가 나옵니다.



Q. categoryData를 담을 때 productData.category처럼 접근은 불가능한가요?
A. .을 통해 접근할 때는 정확한 키 이름이 필요합니다. category는 변수지만 productData.category로 작성시 categoryData에는 productData에 category 키를 찾게 되어 없기 때문에 언제나 null값이 출력됩니다.(3번 이미지)
-실습: 존재하지 않는 URL 접근 시 처리 방법으로, 지정하지 않은 경우 전부 오류 페이지로 표시됩니다.
<Route path="*" element={<Error />} />
//--------------------------------------------------
import { useNavigate, Link } from 'react-router-dom';
export default function Error() {
const navigate = useNavigate();
return (
<div style={{ margin: 10 }}>
<h2>접속할 수 없는 URL 입니다.</h2>
</div>
);
}
-useNavigate은 Javascript 코드로 페이지를 이동시키는 훅으로, 인자 값으로 이동하려는 URL 주소를 넣어줍니다. <Link>는 클릭하면 바로 이동하지만, useNavigate 훅은 조건/이벤트 처리 후 코드를 이동할 때 사용됩니다. 주의할 점은 useNavigate() 훅은 BrowserRouter 밖에서 사용하면 에러가 발생합니다.
navigate('/about'); // 기본 이동
navigate(-1); // 뒤로가기
navigate(1); // 앞으로가기
navigate('/home', { replace: true });// replace: true → 히스토리에 안 쌓임, 뒤로가기 막을 때 사용
navigate('/detail', { state: { from: 'search' } }); // state → URL에 안 보이는 데이터 전달
navigate(`/product/${category}/${id}`); //파라미터와 함께 이동 가능
-> useNavigate() 함수는 이동 기록을 대체해 뒤로가기가 불가능하게 하는 replace:true 옵션이나 이동 시 데이터를 전달하는 state 옵션이 있습니다. state의 경우 useLocation() 훅으로 받은 페이지에서 state를 확인할 수 있습니다.
- useLocation()은 현재 URL의 정보를 객체로 반환해주는 훅으로, useParams가 파라미터만 꺼낸다면, useLocation은 URL 전체 정보를 객체로 반환합니다.
const location = useLocation();
// URL이 /product/books?sort=price#section1 이면
{
pathname: "/product/books", // 현재 경로
search: "?sort=price", // 쿼리스트링
hash: "#section1", // # 이후 값
state: null, // navigate로 전달한 데이터
}
- React에서는 state는 새로고침하면 사라지기 때문에 웹 스토리지에 저장하여 새로고침해도 데이터가 유지될 수 있게합니다. 웹스토리지에는 sessionStorage와 localStorage, 두가지 종류가 있습니다. 윈도우에 웹 페이지가 로드되면 세션 스토리와 로컬 스토리지는 자동으로 생성됩니다.
-> sessionStorage는 탭 닫으면 삭제되며, 같은 탭 안에서만 공유가 가능한 임시 데이터입니다. 다단계 폼을 임시저장하거나 결제 진행 상태, 일회성 데이터 등을 저장합니다.
-> localStorage는 직접 삭제 전까지는 영구 유지되며 같은 브라우저 전체에서 공유됩니다. 사용자 설정(다크모드)나 자동로그인 토큰, TodoList 데이터와 같은 것들을 저장합니다.
| 프로퍼티 | 설명 | r/w |
| length | 스토리지에 저장된 아이템의 개수 | r |
| [] | ['키']로 아이템의 값을 읽거나 저장하는 연산자 | r/w |
| 메소드 | 설명 |
| key(index) | index 위치에 저장된 아이템의 '키' 문자열 반환 |
| setItem(key, val) | (key,val) 아이템을 스토리지에 저장, key와 val 모두 문자열 |
| getItem(key) | 문자열 key의 아이템을 찾아 값 문자열 리턴, 아이템 없으면 null 리턴 |
| removeItem(key) | 문자열 key의 아이템 삭제 |
| clear() | 스토리지의 모든 아이템 삭제 |
localStorage.setItem('key', 'value'); // 저장
localStorage['key'] = 'value';
localStorage.getItem('key'); // "value" , 읽기(없는 키면 null 반환)
localStorage.removeItem('key'); // 삭제
localStorage.clear(); // 전체 삭제
-> sessionStorage와 localStorage는 동일한 방식으로 사용이 가능합니다.
- 실습: todolist 실습

-> App. jsx
import { useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import NavBar from './components/NavBar';
import Home from './components/Home';
import TodoList from './components/TodoList';
import TodoViewer from './components/TodoViewer';
import Error from './components/Error';
export default function App() {
return (
<BrowserRouter>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todolist" element={<TodoList />} />
<Route path="/todolist/:id" element={<TodoViewer />} />
<Route path="*" element={<Error />} />
</Routes>
</BrowserRouter>
);
}
-> NavBar. jsx
import { Link } from 'react-router-dom';
export default function NavBar() {
return (
<nav style={{ padding: '10px', backgroundColor: '#f5f5f5', marginBottom: '20px' }}>
<Link to="/" style={{ marginRight: '10px' }}> Home </Link>
<Link to="/todolist" style={{ marginRight: '10px' }}> TodoList </Link>
</nav>
);
}
• Link는 React Router의 컴포넌트로, 페이지 이동할 때 사용하며 to 속성은 이동할 경로를 나타냅니다.
-> Home.jsx, Error.jsx
export default function Home() {
return (
<div style={{ margin: 10 }}>
<h2>Home</h2>
<p>안녕하세요!</p>
</div>
);
}
//---------------------------------
import { Link } from 'react-router-dom';
export default function Error() {
return (
<div style={{ margin: 10 }}>
<h2>접속할 수 없는 URL 입니다.</h2>
<Link to="/" style={{ marginRight: '10px' }}> Home으로 이동 </Link>
</div>
);
}
-> TodoList.jsx
import { useState, useEffect, useContext } from "react";
import { useNavigate } from 'react-router-dom';
//import AuthContext from '../context/AuthContext';
//import "./style.css";
function TodoList(){
const navigate = useNavigate();
const [todos, setTodos] = useState( ( ) => {
// localStorage에서 todos state의 초기값을 가져옴
const saved = localStorage.getItem('my-todos');
/*
if(saved){
console.log(saved)
console.log(JSON.parse(saved))
}
*/
return saved ? JSON.parse(saved) : [];
});
const [input, setInput] = useState("");
// todos가 변경될 때마다 localStorage에 저장
useEffect(() => {
localStorage.setItem('my-todos', JSON.stringify(todos));
}, [todos]);
const changeInput = (str) => {
setInput(str);
//console.log(`[${input}]`);
};
const addTodo = () => {
if(input.trim()==="") return;
const todo_item = {
id: new Date(),//너무 큰 객체라고 생각해 밀리초로 설정하고 싶으면 id: Date.now()로 사용
text: input,
checked: false,
}
setTodos([...todos,todo_item]);
setInput("");
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
}
const toggleComplete = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo
)); //todo_item 역시 객체이기 때문에 수정할 때 스프레드 구문을 사용해 일부를 수정해야합니다.
}
const remainingTodos = todos.filter(todo=>!todo.checked).length;
return(
<div className="container">
<h2>할 일 목록</h2>
<div className="input-container">
<input type="text"
value={input}
onChange={(e)=>changeInput(e.target.value)}/>
<button onClick={addTodo}>추가</button>
</div>
<p>남은 할 일: {remainingTodos}개</p>
<ul className="todo-list" >
{
todos.map((todo, index) => ( //index는 map/filter가 자동으로 주는 순번
<li key={todo.id} className="todo-item">
<input type="checkbox"
checked={todo.checked}
onChange={()=>toggleComplete(todo.id)}/>
<span style={{textDecoration: todo.checked ? 'line-through' : 'none'}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
삭제
</button>
<button onClick={(e) => {navigate("/todolist/" + todo.id)}} style={{ margin:5 }}>
{/* </button><button onClick={() => navigate("/todolist/" + todo.id)} style={{ margin:5 }}> */}
상세보기
</button>
</li>
))
}
</ul>
</div>
)
}
export default TodoList;
• todos state를 초기화할 때는 localStorage에 my-todos라는 키로 저장된 값을 saved에 저장해준 뒤 사용할 때는 JSON 문자열을 배열로 변환하고 없으면 빈 배열을 반환합니다.

• useEffect(...,[todos])는 todos가 변경될 때마다 자동 실행되며 배열 todos -> 문자열(JSON 형식)으로 변환하기 위해서 JSON.stringify를 해줘서 my-todos에 넣어줍니다.
• 상세보기에서 navigate로 이동할 때 '/todolist/'+todo.id로 상세페이지로 이동합니다.
-> TodoViewer.jsx
import { useNavigate, useParams } from 'react-router-dom';
export default function TodoViewer() {
const navigate = useNavigate();
const { id } = useParams();
// localSotrage에서 todo 목록 데이터 읽어오기
const saved = localStorage.getItem('my-todos');
const todos = saved ? JSON.parse(saved) : [];
console.log(todos);
const todo_item = {};
todos.map((item) => {
if (item.id == id) {
todo_item.id = item.id;
todo_item.text = item.text;
todo_item.checked = item.checked;
}
});
return (
<div style={{ margin: 10 }}>
<p>- ID : {id} </p>
<p>- 할일 내용 : {todo_item.text} </p>
<p>- 할일 여부 : {todo_item.checked ? " 완료" : "미완료"}</p>
<input type="button" onClick={() => navigate(-1)} value="뒤로가기" />
</div>
);
}
• todo_item이라는 빈 배열을 만든 다음에 localStorage에서 빼온 my-todos를 문자열 -> JSON 배열로 변환 시켜준 뒤 useParams 훅으로 가져온 id 값과 일치하는 my-todos의 item의 값을 넣어줍니다.
• navigate(-1)을 통해 뒤로가기 버튼을 만들어줍니다.
'Programming Language > JavaScript&TypeScript' 카테고리의 다른 글
| [JS/React] 리스트와 키, 입력 폼 (0) | 2026.04.18 |
|---|---|
| [JS/React] 이벤트 핸들링 & 조건부 렌더링 (0) | 2026.04.12 |
| [JS/React] JSX(4): Lifecycle, Hook (0) | 2026.04.12 |
| [JS/React] JSX(3): Component 실습, State (0) | 2026.04.12 |
| [JS/React] JSX(2): Component, Element (1) | 2026.04.10 |