icon
6장 : React 라우팅 기초

라우트 파라미터와 쿼리 문자열

웹 애플리케이션에서 특정 리소스의 상세 정보나 검색 결과 필터링 등 데이터를 전달하는 데 매우 중요한 두 가지 방법, 라우트 파라미터(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
// 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
// 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.jsProductList 컴포넌트와 라우트를 추가합니다.

src/App.js
// 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
// 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 RouterRoute 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을 통해 데이터를 전달하는 두 가지 주요 방법인 라우트 파라미터와 쿼리 문자열의 개념, 사용법, 그리고 각각의 적절한 활용 시점을 상세하게 배웠습니다. 특히 useParamsuseSearchParams 훅의 실용적인 사용법을 익혔습니다.

이제 여러분은 리액트 애플리케이션에서 URL을 통해 다양한 데이터를 효율적으로 관리하고, 이를 기반으로 동적인 콘텐츠를 렌더링할 수 있는 중요한 기술을 습득했습니다. 다음 장에서는 React Router의 또 다른 강력한 기능인 중첩 라우팅(Nested Routing)Outlet 컴포넌트에 대해 알아보겠습니다.