라우트 파라미터와 쿼리 문자열
웹 애플리케이션에서 특정 리소스의 상세 정보나 검색 결과 필터링 등 데이터를 전달하는 데 매우 중요한 두 가지 방법, 라우트 파라미터(Route Parameters) 와 쿼리 문자열(Query Strings) 에 대해 더 깊이 있게 알아보겠습니다.
이 두 가지 방식은 모두 URL을 통해 데이터를 전달하지만, 사용 목적과 방법에 명확한 차이가 있습니다. 이 장을 통해 각 방식의 특징을 이해하고 적절하게 활용하는 방법을 배울 것입니다.
라우트 파라미터(Route Parameters)
라우트 파라미터는 URL 경로의 일부분으로 데이터를 포함시키는 방식입니다. 주로 특정 리소스의 고유 식별자(ID) 를 전달하여 해당 리소스의 상세 정보를 불러올 때 사용됩니다.
특징
- 경로의 일부: URL 경로의 계층 구조에 포함됩니다.
- 예:
/users/123
,/products/nike-shoes
- 예:
- 고유 식별자: 주로 특정 항목을 식별하는 데 사용됩니다.
- 필수적 데이터: 해당 경로의 의미를 완성하는 데 필수적인 데이터로 간주됩니다.
- 정의 방식: React Router의
Route
컴포넌트path
속성에:parameterName
형태로 정의합니다.
사용 예시 (이전 장 ProductDetail 확장)
이전 장에서 만들었던 ProductDetail
컴포넌트와 라우트를 다시 살펴보겠습니다.
// src/App.js (일부)
import { Routes, Route } from 'react-router-dom';
import ProductDetail from './pages/ProductDetail';
function App() {
return (
<Routes>
{/* id는 라우트 파라미터 이름입니다. */}
<Route path="/product/:id" element={<ProductDetail />} />
</Routes>
);
}
// src/pages/ProductDetail.js
import React from 'react';
import { useParams } from 'react-router-dom';
function ProductDetail() {
// useParams 훅을 사용하여 URL에서 'id' 파라미터 값을 추출
const { id } = useParams();
// 실제 앱에서는 이 id를 사용하여 서버에서 제품 데이터를 가져올 것입니다.
// 여기서는 가상 데이터 사용
const productData = {
'123': { name: 'React 신발', description: 'React 개발자를 위한 특별한 신발입니다.' },
'456': { name: '리액트 후드티', description: '깔끔한 리액트 로고가 새겨진 후드티입니다.' }
};
const product = productData[id];
if (!product) {
return <div style={{ padding: '20px', textAlign: 'center' }}>제품을 찾을 수 없습니다. (ID: {id})</div>;
}
return (
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px', margin: '20px auto', maxWidth: '600px', textAlign: 'center', backgroundColor: '#f9f9f9' }}>
<h2 style={{ color: '#007bff' }}>{product.name}</h2>
<p style={{ fontSize: '1.1em', color: '#555' }}>{product.description}</p>
<p style={{ fontSize: '0.9em', color: '#777' }}>제품 ID: <span style={{ fontWeight: 'bold' }}>{id}</span></p>
</div>
);
}
export default ProductDetail;
path="/product/:id"
: 여기서:id
는 라우트 파라미터입니다.id
라는 이름으로 URL의 해당 위치에 오는 값을 받을 수 있습니다.useParams()
: React Router에서 제공하는 훅으로, 현재 라우트의 모든 파라미터 값을 객체 형태로 반환합니다.const { id } = useParams();
처럼 비구조화 할당으로 쉽게 접근할 수 있습니다.- 예시 URL:
http://localhost:3000/product/123
,http://localhost:3000/product/banana
쿼리 문자열(Query Strings)
쿼리 문자열은 URL의 ?
뒤에 key=value
형태로 데이터를 포함시키는 방식입니다. 주로 검색, 필터링, 정렬, 페이지네이션 등 선택적이거나 부가적인 데이터를 전달할 때 사용됩니다.
특징
- URL의 부가적인 부분: 경로의 의미를 직접적으로 바꾸지는 않습니다.
?
로 시작,&
로 연결: 여러 개의 쿼리 파라미터는&
로 연결됩니다.- 예:
/products?category=electronics&sort=price_asc&page=2
- 예:
- 선택적 데이터: 파라미터가 없어도 경로 자체는 유효합니다.
- 정의 방식:
Route
path
에는 명시적으로 정의하지 않고,useSearchParams
훅을 사용하여 접근합니다.
useSearchParams
훅 사용법
React Router v6부터는 쿼리 문자열을 다루기 위해 useSearchParams
훅을 사용합니다. 이 훅은 [searchParams, setSearchParams]
배열을 반환합니다.
searchParams
: URLSearchParams 객체의 인스턴스로, 쿼리 파라미터에 접근할 수 있는 메서드를 제공합니다.searchParams.get('key')
: 특정key
에 해당하는 값 가져오기searchParams.has('key')
: 특정key
가 존재하는지 확인
setSearchParams
: 쿼리 문자열을 업데이트하는 함수입니다. 이 함수를 사용하면 URL이 변경되고 컴포넌트가 다시 렌더링됩니다.
예제: 제품 목록 필터링 및 검색
src/pages/ProductList.js
파일을 만들고 쿼리 문자열을 사용하여 제품 목록을 필터링하고 검색하는 기능을 구현해 봅시다.
// src/pages/ProductList.js
import React, { useState, useEffect } from 'react';
import { useSearchParams, Link } from 'react-router-dom';
const allProducts = [
{ id: '1', name: '노트북', category: 'electronics', price: 1200000 },
{ id: '2', name: '마우스', category: 'electronics', price: 30000 },
{ id: '3', name: '키보드', category: 'electronics', price: 80000 },
{ id: '4', name: '책상', category: 'furniture', price: 150000 },
{ id: '5', name: '의자', category: 'furniture', price: 70000 },
{ id: '6', name: '스마트폰', category: 'electronics', price: 900000 },
{ id: '7', name: '모니터', category: 'electronics', price: 300000 },
{ id: '8', name: '램프', category: 'furniture', price: 25000 },
];
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams(); // useSearchParams 훅 사용
const [filteredProducts, setFilteredProducts] = useState([]);
// URL 쿼리 파라미터에서 현재 필터 및 검색어 가져오기
const currentCategory = searchParams.get('category') || '';
const searchQuery = searchParams.get('q') || '';
// 쿼리 파라미터나 검색어 변경 시 제품 목록 필터링
useEffect(() => {
let tempProducts = allProducts;
if (currentCategory) {
tempProducts = tempProducts.filter(
(product) => product.category === currentCategory
);
}
if (searchQuery) {
tempProducts = tempProducts.filter((product) =>
product.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
setFilteredProducts(tempProducts);
}, [currentCategory, searchQuery]); // 의존성 배열에 쿼리 파라미터 값 추가
const handleCategoryChange = (category) => {
// setSearchParams를 사용하여 쿼리 파라미터 업데이트
if (category === '') {
searchParams.delete('category'); // 'category' 쿼리 파라미터 제거
} else {
searchParams.set('category', category); // 'category' 쿼리 파라미터 설정
}
// 검색어가 있다면 유지, 없다면 삭제
if (searchQuery) {
searchParams.set('q', searchQuery);
} else {
searchParams.delete('q');
}
setSearchParams(searchParams); // 변경된 searchParams 객체로 업데이트
};
const handleSearchChange = (e) => {
const newSearchQuery = e.target.value;
if (newSearchQuery) {
searchParams.set('q', newSearchQuery);
} else {
searchParams.delete('q');
}
// 카테고리가 있다면 유지, 없다면 삭제
if (currentCategory) {
searchParams.set('category', currentCategory);
} else {
searchParams.delete('category');
}
setSearchParams(searchParams);
};
return (
<div style={{ padding: '20px', maxWidth: '900px', margin: '20px auto', backgroundColor: '#fff', borderRadius: '10px', boxShadow: '0 4px 15px rgba(0,0,0,0.1)' }}>
<h2 style={{ textAlign: 'center', color: '#4CAF50', marginBottom: '30px' }}>제품 목록</h2>
{/* 필터링 및 검색 UI */}
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
<button
onClick={() => handleCategoryChange('')}
style={{ margin: '5px', padding: '8px 15px', borderRadius: '5px', border: currentCategory === '' ? '2px solid #4CAF50' : '1px solid #ccc', backgroundColor: currentCategory === '' ? '#e8f5e9' : 'white', cursor: 'pointer' }}
>
All
</button>
<button
onClick={() => handleCategoryChange('electronics')}
style={{ margin: '5px', padding: '8px 15px', borderRadius: '5px', border: currentCategory === 'electronics' ? '2px solid #4CAF50' : '1px solid #ccc', backgroundColor: currentCategory === 'electronics' ? '#e8f5e9' : 'white', cursor: 'pointer' }}
>
Electronics
</button>
<button
onClick={() => handleCategoryChange('furniture')}
style={{ margin: '5px', padding: '8px 15px', borderRadius: '5px', border: currentCategory === 'furniture' ? '2px solid #4CAF50' : '1px solid #ccc', backgroundColor: currentCategory === 'furniture' ? '#e8f5e9' : 'white', cursor: 'pointer' }}
>
Furniture
</button>
<input
type="text"
placeholder="제품 검색..."
value={searchQuery}
onChange={handleSearchChange}
style={{ marginLeft: '20px', padding: '8px 12px', borderRadius: '5px', border: '1px solid #ccc', width: '200px' }}
/>
</div>
{/* 제품 목록 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '20px' }}>
{filteredProducts.length > 0 ? (
filteredProducts.map((product) => (
<div
key={product.id}
style={{
border: '1px solid #eee',
borderRadius: '8px',
padding: '15px',
backgroundColor: '#fefefe',
boxShadow: '0 2px 5px rgba(0,0,0,0.05)',
textAlign: 'center',
}}
>
<h3 style={{ margin: '0 0 10px 0', color: '#333' }}>{product.name}</h3>
<p style={{ fontSize: '0.9em', color: '#666', margin: '0' }}>{product.category}</p>
<p style={{ fontSize: '1.1em', fontWeight: 'bold', color: '#007bff' }}>{product.price.toLocaleString()}원</p>
{/* Link를 사용하여 상세 페이지로 이동 (라우트 파라미터 사용) */}
<Link to={`/product/${product.id}`} style={{ display: 'inline-block', marginTop: '10px', padding: '8px 12px', backgroundColor: '#007bff', color: 'white', textDecoration: 'none', borderRadius: '5px' }}>
상세 보기
</Link>
</div>
))
) : (
<p style={{ gridColumn: '1 / -1', textAlign: 'center', color: '#888' }}>검색 결과가 없습니다.</p>
)}
</div>
</div>
);
}
export default ProductList;
App.js
에 라우트 추가
src/App.js
에 ProductList
컴포넌트와 라우트를 추가합니다.
// src/App.js (수정)
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import ProductDetail from './pages/ProductDetail';
import ProductList from './pages/ProductList'; // ProductList 컴포넌트 임포트
function App() {
return (
<BrowserRouter>
<div className="App">
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/product/:id" element={<ProductDetail />} />
<Route path="/products" element={<ProductList />} /> {/* 제품 목록 라우트 추가 */}
<Route path="*" element={
<div style={{ textAlign: 'center', padding: '50px', color: '#888' }}>
<h2>404 Not Found</h2>
<p>페이지를 찾을 수 없습니다.</p>
</div>
} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
Navbar.js
에도 ProductList
로 이동하는 링크를 추가하는 것이 좋습니다.
// src/components/Navbar.js (수정)
import React from 'react';
import { NavLink } from 'react-router-dom';
function Navbar() {
const navLinkStyle = ({ isActive }) => {
return {
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? '#007bff' : '#333',
textDecoration: 'none',
padding: '10px 15px',
margin: '0 10px',
borderRadius: '5px',
transition: 'all 0.3s ease',
backgroundColor: isActive ? '#e7f5ff' : 'transparent',
};
};
return (
<nav style={{
backgroundColor: '#f8f9fa',
padding: '15px 0',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)',
display: 'flex',
justifyContent: 'center',
gap: '20px'
}}>
<NavLink to="/" style={navLinkStyle}>Home</NavLink>
<NavLink to="/about" style={navLinkStyle}>About</NavLink>
<NavLink to="/contact" style={navLinkStyle}>Contact</NavLink>
<NavLink to="/products" style={navLinkStyle}>Product List</NavLink> {/* 추가 */}
<NavLink to="/product/123" style={navLinkStyle}>Product Detail (예시)</NavLink>
</nav>
);
}
export default Navbar;
실행 및 확인
http://localhost:3000/products
로 이동하여 제품 목록을 확인합니다.- "Electronics" 또는 "Furniture" 버튼을 클릭하여 URL이
http://localhost:3000/products?category=electronics
등으로 변하고 필터링이 적용되는지 확인합니다. - 검색창에 "노트북" 또는 "의자" 등을 입력하여 URL이
http://localhost:3000/products?q=노트북
또는http://localhost:3000/products?category=electronics&q=노트북
등으로 변하고 검색이 적용되는지 확인합니다. - 쿼리 파라미터가 변경될 때마다 페이지 전체가 새로고침되지 않고 컴포넌트만 업데이트되는 것을 확인합니다.
라우트 파라미터 vs 쿼리 문자열
두 방식 모두 URL을 통해 데이터를 전달하지만, 각각의 용도와 컨벤션이 다릅니다.
특징 | 라우트 파라미터 (/users/:id ) | 쿼리 문자열 (/products?category=X&sort=Y ) |
---|---|---|
목적 | 특정 리소스의 고유 식별 또는 계층적 경로 | 선택적 필터링, 정렬, 검색, 페이지네이션 등 부가적인 정보 |
위치 | URL 경로의 일부 | URL의 ? 뒤에 추가 |
형태 | /경로/:key | ?key=value&key2=value2 |
필수 여부 | 해당 경로의 의미를 구성하는 데 필수적 (없으면 404) | 해당 경로에서 선택적 (없어도 경로 자체는 유효) |
예시 | /users/123 , /posts/how-to-react | /products?category=shoes , /search?q=react&page=1 |
React Router | Route path="/path/:key" , useParams() | Route path="/path" , useSearchParams() |
간단한 규칙
- RESTful API와 유사하게 리소스를 식별해야 한다면 라우트 파라미터를 사용하세요. (예:
/users/5
,/books/harry-potter
) - 필터링, 정렬, 검색어, 페이지 번호 등 부가적인 상태를 전달해야 한다면 쿼리 문자열을 사용하세요. (예:
/books?author=JKR&genre=fantasy
,/search?query=react&page=2
)
"라우트 파라미터와 쿼리 문자열"은 여기까지입니다. 이 장에서는 React Router에서 URL을 통해 데이터를 전달하는 두 가지 주요 방법인 라우트 파라미터와 쿼리 문자열의 개념, 사용법, 그리고 각각의 적절한 활용 시점을 상세하게 배웠습니다. 특히 useParams
와 useSearchParams
훅의 실용적인 사용법을 익혔습니다.
이제 여러분은 리액트 애플리케이션에서 URL을 통해 다양한 데이터를 효율적으로 관리하고, 이를 기반으로 동적인 콘텐츠를 렌더링할 수 있는 중요한 기술을 습득했습니다. 다음 장에서는 React Router의 또 다른 강력한 기능인 중첩 라우팅(Nested Routing) 과 Outlet
컴포넌트에 대해 알아보겠습니다.