Next.js

[Next.js] Next.js + TypeScript에서의 라우팅 시 데이터 전달 방법 총정리

프론트 개발자 김현중 2025. 3. 18. 13:36
728x90

Next.js 라우팅 시 데이터 전달 방법 개요

Next.js에서 페이지 간 이동 시 데이터를 전달하는 방법은 여러 가지가 있다.

이전 글에서 Link 컴포넌트와 useRouter 훅을 사용한 페이지 이동 방법에 대해 알아보았다면, 이번에는 그 과정에서 데이터를 어떻게 전달할 수 있는지 알아보려고 한다.

(보시지 못한 분들은 아래 글을 참고 바란다.)

 

[Next.js] Next.js + TypeScript에서의 페이지 이동 방법 총정리

Next.js에서의 페이지 이동 방법 개요Next.js에서 페이지 간 이동을 구현하는 방법은 크게 두 가지가 있다.Link 컴포넌트 사용하기useRouter 훅을 사용하여 프로그래매틱하게 라우팅하는 방법일반 React

bbin-guuuu.tistory.com

 

페이지 간 데이터를 전달하는 주요 방법으로는 크게 네 가지가 있다.

  1. Query Parameters(쿼리 파라미터)
  2. Path Parameters(경로 파라미터)
  3. State 객체
  4. Context API를 통한 전역 상태 관리

각 방법은 전달하는 데이터의 특성, 보안 요구사항, UX 등에 따라 적합한 상황이 다르다.

하나씩 이해해볼까?


Query Parameters를 이용한 데이터 전달

Query Parameters는 URL의 ? 뒤에 오는 키-값 쌍으로, 페이지 간에 데이터를 전달하는 가장 기본적인 방법 중 하나이다.

이 방법은 GET 요청처럼 데이터가 URL에 노출되어야 하는 경우에 적합하다.

Link 컴포넌트를 사용한 쿼리 파라미터 전달하기

// app/products/page.tsx
import Link from 'next/link';

export default function ProductsPage() {
  return (
    <div>
      <h1>제품 목록</h1>
      <div>
        <Link href="/products/detail?id=1&category=electronics">
          전자제품 1번 상세 보기
        </Link>
      </div>
      <div>
        <Link href={{
          pathname: '/products/detail',
          query: { id: 2, category: 'clothing' },
        }}>
          의류 2번 상세 보기
        </Link>
      </div>
    </div>
  );
}

위 코드에서는 두 가지 방법으로 쿼리 파라미터를 전달하고 있다.

첫 번째는 문자열로 직접 URL을 작성하는 방법이고,

두 번째는 객체 형태로 pathname과 query를 분리하여 전달하는 방법이다.

두 번째 방식이 TypeScript 타입 체킹을 더 잘 지원하므로 복잡한 쿼리 파라미터 구성 시 권장된다!!!

 

useRouter로 쿼리 파라미터 전달하기

// app/search/page.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function SearchPage() {
  const router = useRouter();
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState('all');

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault();
    
    // 쿼리 파라미터로 검색 조건 전달
    const queryString = new URLSearchParams({
      term: searchTerm,
      category: category
    }).toString();
    
    router.push(`/search/results?${queryString}`);
  };

  return (
    <div>
      <h1>제품 검색</h1>
      <form onSubmit={handleSearch}>
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="검색어 입력"
        />
        <select 
          value={category} 
          onChange={(e) => setCategory(e.target.value)}
        >
          <option value="all">전체</option>
          <option value="electronics">전자제품</option>
          <option value="clothing">의류</option>
        </select>
        <button type="submit">검색</button>
      </form>
    </div>
  );
}

 

 

이 코드에서는 useRouter를 사용하여 프로그래매틱하게 쿼리 파라미터를 구성한다.

URLSearchParams 객체를 사용하면 쿼리 파라미터를 안전하게 구성할 수 있으며, 자동으로 인코딩도 처리해준다.

쿼리 파라미터 접근 및 사용???

Next.js App Router에서는 쿼리 파라미터에 접근하는 방법이 이전 버전과 조금 다르다.

// app/products/detail/page.tsx
'use client';

import { useSearchParams } from 'next/navigation';

export default function ProductDetailPage() {
  const searchParams = useSearchParams();
  
  // 쿼리 파라미터 가져오기
  const id = searchParams.get('id');
  const category = searchParams.get('category');
  
  return (
    <div>
      <h1>제품 상세 정보</h1>
      <p>제품 ID: {id}</p>
      <p>카테고리: {category}</p>
    </div>
  );
}

useSearchParams 훅을 사용하면 현재 URL의 쿼리 파라미터에 쉽게 접근할 수 있다.

이 훅은 SearchParams 객체를 반환하며, 이 객체는 URL의 쿼리 파라미터를 다루기 위한 메서드를 제공한다.

 

서버 컴포넌트에서는 다음과 같이 쿼리 파라미터에 접근할 수 있다.

// app/products/detail/page.tsx (서버 컴포넌트)
export default function ProductDetailPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) {
  const id = searchParams.id;
  const category = searchParams.category;
  
  return (
    <div>
      <h1>제품 상세 정보 (서버 컴포넌트)</h1>
      <p>제품 ID: {id}</p>
      <p>카테고리: {category}</p>
    </div>
  );
}

서버 컴포넌트에서는 props로 searchParams 객체를 받아 쿼리 파라미터에 접근할 수 있다.

이 방법을 사용하면 클라이언트에서 자바스크립트를 실행하지 않고도 쿼리 파라미터를 기반으로 페이지를 렌더링할 수 있다.


Path Parameters(동적 라우팅)를 활용한 데이터 전달

Path Parameters는 URL 경로의 일부로 데이터를 전달하는 방식이다.

Next.js에서는 파일 이름이나 폴더 이름을 [parameter] 형태로 지정하여 동적 라우팅을 구현할 수 있다.

아랫글에도 잠시 설명은하지만 다시한번 정리해보고자 한다.

 

[Next.js] 파일 시스템 기반 라우팅 개념 정리

Next.js 라우팅의 기본 개념Next.js의 가장 큰 특징 중 하나는 파일 시스템 기반 라우팅(File-system based routing)이다.이 방식은 React의 SPA(Single Page Application)에서 흔히 사용되는 React Router와 같은 별도의

bbin-guuuu.tistory.com

 

동적 라우팅 구성

App Router에서는 폴더 구조를 사용하여 동적 라우팅을 구성한다.

app/
  products/
    [id]/
      page.tsx

이 구조에서 [id]는 동적 세그먼트로, URL의 해당 위치에 어떤 값이든 올 수 있다.

예를 들어, /products/123, /products/456 등의 URL은 모두 같은 페이지 컴포넌트로 라우팅된다.

Link 컴포넌트로 경로 파라미터 전달하기

// app/products/page.tsx
import Link from 'next/link';

export default function ProductsPage() {
  const products = [
    { id: 1, name: '무선 이어폰' },
    { id: 2, name: '스마트워치' },
    { id: 3, name: '태블릿' }
  ];

  return (
    <div>
      <h1>제품 목록</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <Link href={`/products/${product.id}`}>
              {product.name}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

각 제품의 ID를 URL 경로의 일부로 포함시켜 해당 제품의 상세 페이지로 이동하게 한다.

이 방식은 RESTful API 설계 원칙과 일치하기 때문에 리소스를 식별하는 데 자주 사용된다.

useRouter로 경로 파라미터 전달하기

// app/admin/page.tsx
'use client';

import { useRouter } from 'next/navigation';

export default function AdminPage() {
  const router = useRouter();
  const products = [
    { id: 1, name: '무선 이어폰' },
    { id: 2, name: '스마트워치' },
    { id: 3, name: '태블릿' }
  ];

  const handleEdit = (productId: number) => {
    router.push(`/products/${productId}/edit`);
  };

  return (
    <div>
      <h1>관리자 페이지</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name}
            <button onClick={() => handleEdit(product.id)}>
              편집하기
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

useRouter 훅을 사용하여 프로그래매틱하게 동적 경로로 이동하는 방법을 보여준다.

버튼 클릭 이벤트와 같은 사용자 상호작용에 응답하여 페이지 이동을 처리할 때 유용하다.

경로 파라미터 접근 및 사용

// app/products/[id]/page.tsx
export default function ProductDetailPage({
  params,
}: {
  params: { id: string };
}) {
  return (
    <div>
      <h1>제품 상세 정보</h1>
      <p>제품 ID: {params.id}</p>
    </div>
  );
}

동적 라우팅이 적용된 페이지 컴포넌트는 params 객체를 props로 받는다.

이 객체는 URL 경로에서 추출된 파라미터 값을 포함한다.

위 예제에서는 [id] 폴더에 의해 생성된 id 파라미터에 접근하고 있다.

 

클라이언트 컴포넌트에서는 useParams 훅을 사용하여 경로 파라미터에 접근할 수 있다. 예시코드를 참고하자.

// app/products/[id]/ClientComponent.tsx
'use client';

import { useParams } from 'next/navigation';

export default function ClientComponent() {
  const params = useParams();
  const id = params.id;
  
  return (
    <div>
      <h2>클라이언트 컴포넌트</h2>
      <p>제품 ID: {id}</p>
    </div>
  );
}

State를 활용한 데이터 전달

때로는 URL에 데이터를 노출시키지 않고 페이지 간에 데이터를 전달해야 할 수 있다.

이런 경우 next/navigation의 router.push 메서드를 state 옵션과 함께 사용할 수 있다.

// app/products/client-page.tsx
'use client';

import { useRouter } from 'next/navigation';

export default function ProductsPage() {
  const router = useRouter();
  
  const product = {
    id: 123,
    name: '프리미엄 헤드폰',
    price: 299000,
    description: '고품질 사운드의 프리미엄 헤드폰',
    secret: '비공개 제품 코드: XYZ123'
  };

  const handleViewDetails = () => {
    // state를 사용하여 URL에 노출되지 않는 정보 전달
    router.push(`/products/${product.id}`, {
      state: { productData: product }
    });
  };

  return (
    <div>
      <h1>제품 정보</h1>
      <button onClick={handleViewDetails}>
        상세 정보 보기
      </button>
    </div>
  );
}
🚨 그러나 주의할 점! 🚨
현재 App Router에서는 router.push의 state 옵션이 정식으로 지원되지 않고 있다.

이 코드는 Pages Router에서 작동하는 방식을 보여주는 것으로,

App Router로 마이그레이션할 계획이라면 아래와 같은 대안을 고려해야 한다.

App Router에서의 상태 관리 대안

App Router에서 페이지 간 상태를 공유하기 위한 대안으로는 다음과 같은 방법이 있다.

  • URL 쿼리 파라미터 사용 (단, 민감한 정보는 포함하지 않음)
  • SessionStorageLocalStorage 활용
  • Context API 사용
  • 상태 관리 라이브러리 사용 (Redux, Zustand 등)

Context API를 활용한 전역 상태 관리

여러 페이지나 컴포넌트에서 공유해야 하는 데이터가 있는 경우, React의 Context API를 활용하여 전역 상태를 관리할 수 있다.

이 경우에는 예시코드들로 살펴보면 좋을 것 같다.

Context 생성

// app/context/ProductContext.tsx
'use client';

import { createContext, useContext, useState, ReactNode } from 'react';

// 타입 정의
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

interface ProductContextType {
  selectedProduct: Product | null;
  setSelectedProduct: (product: Product | null) => void;
}

// Context 생성
const ProductContext = createContext<ProductContextType | undefined>(undefined);

// Provider 컴포넌트
export function ProductProvider({ children }: { children: ReactNode }) {
  const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
  
  return (
    <ProductContext.Provider value={{ selectedProduct, setSelectedProduct }}>
      {children}
    </ProductContext.Provider>
  );
}

// Custom Hook
export function useProductContext() {
  const context = useContext(ProductContext);
  if (context === undefined) {
    throw new Error('useProductContext must be used within a ProductProvider');
  }
  return context;
}

 

Context Provider 적용

// app/layout.tsx
import { ProductProvider } from './context/ProductContext';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <ProductProvider>
          {children}
        </ProductProvider>
      </body>
    </html>
  );
}

Context 사용 예시

// app/products/client-page.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useProductContext } from '../context/ProductContext';

export default function ProductsPage() {
  const router = useRouter();
  const { setSelectedProduct } = useProductContext();
  
  const products = [
    { id: 1, name: '프리미엄 헤드폰', price: 299000, description: '고품질 사운드의 프리미엄 헤드폰' },
    { id: 2, name: '블루투스 스피커', price: 150000, description: '강력한 베이스의 블루투스 스피커' }
  ];
  
  const handleSelectProduct = (product) => {
    // Context에 선택한 제품 정보 저장
    setSelectedProduct(product);
    // 제품 상세 페이지로 이동
    router.push(`/products/${product.id}`);
  };
  
  return (
    <div>
      <h1>제품 목록</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name}
            <button onClick={() => handleSelectProduct(product)}>
              상세 보기
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
// app/products/[id]/detail-client.tsx
'use client';

import { useProductContext } from '../../context/ProductContext';

export default function ProductDetailClient() {
  const { selectedProduct } = useProductContext();
  
  if (!selectedProduct) return <div>제품 정보를 찾을 수 없습니다.</div>;
  
  return (
    <div>
      <h1>{selectedProduct.name}</h1>
      <p>가격: {selectedProduct.price.toLocaleString()}원</p>
      <p>설명: {selectedProduct.description}</p>
    </div>
  );
}

Context API를 사용하면 전역 상태를 통해 페이지 간에 데이터를 공유할 수 있다.

이 방법은 여러 컴포넌트에서 같은 데이터에 접근해야 하는 경우에 유용하지만,

페이지를 새로고침하면 Context 상태가 초기화된다는 점을 염두에 두어야 한다.


각 데이터 전달 방식의 장단점 살펴보기

Query Parameters

장점:

  • URL에 상태가 반영되어 북마크와 공유가 가능
  • 서버 컴포넌트에서 직접 접근 가능
  • SEO에 유리

단점:

  • URL 길이 제한이 있어 대용량 데이터에 적합하지 않음
  • 민감한 정보를 포함할 수 없음
  • 복잡한 객체나 배열을 전달하기 어려움

Path Parameters

장점:

  • RESTful 리소스 식별에 적합
  • URL이 간결하고 직관적
  • 서버 컴포넌트에서 직접 접근 가능

단점:

  • 단순한 값(주로 ID나 슬러그)만 전달 가능
  • 여러 값을 전달하기에 적합하지 않음

State 객체 (LocalStorage 등)

장점:

  • URL에 데이터가 노출되지 않음
  • 복잡한 객체도 전달 가능
  • 더 많은 양의 데이터 전달 가능

단점:

  • 페이지 새로고침 시 데이터 유지 관리 필요
  • 클라이언트 사이드에서만 접근 가능
  • 북마크나 공유가 어려움

Context API

장점:

  • 컴포넌트 트리 전체에서 데이터 접근 가능
  • 복잡한 상태 관리 가능
  • 컴포넌트 간 prop drilling 방지

단점:

  • 페이지 새로고침 시 상태 초기화
  • 설정이 다소 복잡
  • 남용 시 성능 이슈 가능성
728x90