[Next.js] Routing - App Router

문지은·2023년 12월 28일
0

Next.js - App Router

목록 보기
1/20
post-thumbnail

Next.js 라우팅 동작 방식

  • Next.js의 앱 라우터는 ‘관습 대 설정(Convention over Configuration)’을 사용하여 라우트를 정의한다.
    • 이는 page.tsx, layout.tsx, loading.tsx, route.tsx 등 특별한 파일을 찾아 사용.

      파일명역할
      layout세그먼트의 메인 컨텐츠와 하위 세그먼트의 공용 레이아웃 UI
      page세그먼트의 메인 컨텐츠 UI
      loading세그먼트의 메인 컨텐츠와 하위 세그먼트의 로딩 UI
      not-found세그먼트의 메인 컨텐츠와 하위 세그먼트의 Not Found UI
      error세그먼트의 메인 컨텐츠와 하위 세그먼트의 에러 UI
      global-error전역 에러 UI
      route서버 API 엔드포인트
      template특별하게 재사용될 수 있는 레이아웃 UI
      default패러럴 라우트의 폴백 UI
    • 각 File Convention은 공식문서에서 확인할 수 있다.

  • 앱 라우터를 사용하면 페이지와 그 구성요소들(예: 컴포넌트, 서비스 등)을 공동 위치에 배치할 수 있어 프로젝트를 더 잘 조작할 수 있다.
    • 중앙 집중식 컴포넌트 디렉토리에 모든 컴포넌트를 넣을 필요가 없다!
  • 파일 시스템 기반의 라우팅으로, 외부에서 접근 가능한 파일은 page.tsx 파일
    • app/user/page.tsx/users
    • app/products/page.tsx/products
    • app/courses/page.tsx/courses
  • 다음은 사용자 목록을 표시하는 페이지이다.
    • 코드의 가독성을 높이기 위해 사용자 목록 부분을 추출해 UserTable 컴포넌트로 분리해보자.

app/users/page.tsx

import React from "react";

interface User {
  id: number;
  name: string;
}

const UsersPage = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users", {
  });
  const users: User[] = await res.json();

  return (
    <div>
      <div>This is UserPage</div>
      <p>{new Date().toLocaleTimeString()}</p>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UsersPage;
  • users 폴더에 UserTable.tsx 파일을 생성한다.

app/users/UserTable.tsx

import React from "react";

const UserTable = () => {
  return <div>UserTable</div>;
};

export default UserTable;
  • 사용자 목록을 받아오고 렌더링하는 코드를 이동시킨다.

app/users/page.tsx

import React from "react";

interface User {
  id: number;
  name: string;
  email: string;
}

const UserTable = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users", {
    cache: "no-store",
  });
  const users: User[] = await res.json();

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Email</th>
        </tr>
      </thead>
      <tbody>
        {users.map((user) => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>{user.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default UserTable;

app/users/UserTable.tsx

import React from "react";
import UserTable from "./UserTable";

const UsersPage = () => {
  return (
    <div>
      <div>This is UserPage</div>
      <p>{new Date().toLocaleTimeString()}</p>
      <UserTable />
    </div>
  );
};

export default UsersPage;
  • UserTable 컴포넌트는 users 라우터 외에는 재사용 가능성이 없어 components 폴더에 위치시키지 않았다.
    • 재사용 가능한 컴포넌트는 components 폴더에 위치시켜야 한다.
  • 이렇게 App 라우터에서는 page.tsx 파일 내에서 export default 로 선언된 컴포넌트만을 제외하고는 외부로 노출되지 않는다.

Dynamic Routes

  • 하나 이상의 파라미터를 사용하는 라우트
  • 폴더명을 작성할 때 대괄호를 작성하고 대괄호 사이에 매개변수 이름을 작성해 폴더를 생성하면, 동적 라우트를 구현할 수 있다.
    • ex) [id], [userId], [username], …
  • [id]를 매개변수로 하는 UserDetailPage 파일을 작성해보자.

app/users/[id]/page.tsx

import React from "react";

const UserDetailPage = () => {
  return <div>UserDetailPage</div>;
};

export default UserDetailPage;
  • 동적 라우트 페이지 파일에서는 매개변수의 접근이 필수적이다.
    • 이는 해당 매개변수를 추출하여 API 요청과 같은 다양한 상황에서 유용하게 활용될 수 있기 때문이다.
  • 매개변수는 컴포넌트의 props를 통해 접근할 수 있다.
import React from "react";

interface Props {
  params: { id: number };
}

const UserDetailPage = (props: Props) => {
  console.log(props.params.id);
  return <div>UserDetailPage</div>;
};

export default UserDetailPage;
  • Destructuring 문법을 통해 아래와 같이 작성할 수도 있음.
import React from "react";

interface Props {
  params: { id: number };
}
const UserDetailPage = ({ params: { id } }: Props) => {
  return <div>UserDetailPage {id}</div>;
};

export default UserDetailPage;

  • /users/[id]/photos/[photoId] 와 같은 동적 라우트를 생성하고 싶다면?
    • 아래와 같은 폴더 구조로 파일 생성
app
  |- users
	|- [id]
	  |- photos
		|- [photoId]
		  |- page.tsx
import React from "react";

interface Props {
  params: { id: number; photoId: number };
}
const PhotoPage = (props: Props) => {
  return (
    <div>
      PhotoPage {props.params.id} {props.params.photoId}
    </div>
  );
};

export default PhotoPage;

Catch All Segments

  • 아래 예시처럼 여러 매개변수를 동시에 처리해야 할 상황이 있다면 catch all segments를 활용하면 된다.
    • /profile/[username]/[postId]
  • 특정 매개변수 아래의 하위경로를 전부 처리하려면, […매개변수] 형식으로 폴더를 생성하고, 내부에 page.tsx 파일을 생성한다.

app/profile/[…username]/page.tsx

import React from "react";

const ProfilePage = (props) => {
  console.log(props);
  return <div>ProfilePage</div>;
};

export default ProfilePage;
  • /profile/jieun 경로에 접속하여 props를 콘솔에 출력해보자.
    • username 값으로 매개변수가 문자열 배열 형태로 나타나는 것을 확인할 수 있다.
{ params: { username: [ 'jieun' ] }, searchParams: {} }
  • Props 인터페이스를 작성해보자.
    • params는 Next.js에서 정의한 규칙에 따른 프로퍼티이므로 해당 이름을 따라야 한다.
import React from "react";

interface Props {
  params: {
    username: string[];
  };
}
const ProfilePage = (props: Props) => {
  console.log(props);
  return <div>ProfilePage</div>;
};

export default ProfilePage;
  • 이제 매개변수 개수에 따라 작동 상태를 테스트 해보자.
    • 한 개 이상의 매개변수가 있을 때는 정상 렌더링 되지만 매개변수가 없는 경우에는 404 페이지가 렌더링 되는 것을 볼 수 있다.

/profile/jieun/post1

/profile

  • 이는 Catch All Segments 규칙에 의한 것이며, 매개변수가 없는 경우에도 렌더링 되게 하기 위해서는 대괄호를 한 번 더 감싸야 한다.
  • 폴더 […username][[…username]]으로 변경한 후, /profile 에 다시 접속해보자.
    • 정상적으로 렌더링 되는 것을 확인할 수 있다.

QueryString Parameters

  • Query String
    • 웹 URL의 일부로, 주로 웹 페이지에 정보를 전달하는 데 사용되는 변수와 그 값을 포함한다.
    • 이는 ? 문자 뒤에 나타나며, 변수와 값은 = 로 연결되고, 여러 변수들은 & 으로 구분된다.
    • ex) https://example.com/page?name=value&key2=value2
  • Next.js 에서 쿼리스트링은 props의 searchParams 키워드를 통해 접근할 수 있다.
  • 다음과 같이 파일을 작성하고 props를 콘솔에 출력해보자.

app/products/[..slug]/page.tsx

import React from "react";

const ProductPage = (props) => {
  console.log(props);
  return <div>ProductPage</div>;
};

export default ProductPage;

/products/computer?sortOrder=name 에 접속 후 콘솔 확인

{
  params: { slug: [ 'computer' ] },
  searchParams: { sortOrder: 'name' }
}
  • 이제 props 구조를 파악했으니, 인터페이스를 작성해보자.
import React from "react";

interface Props {
  params: { slug: string };
  searchParams: { sortOrder: string };
}

const ProductPage = ({
  params: { slug },
  searchParams: { sortOrder },
}: Props) => {
  return (
    <div>
      ProductPage {slug} {sortOrder}
    </div>
  );
};

export default ProductPage;
  • 정상 렌더링 되는 것을 확인할 수 있다.

  • 이제 실제 API 에서 쿼리스트링을 적용해보자.
  • jsonplaceholder API를 사용하여 사용자 목록을 추출하고 sortOrder 쿼리 스트링 값에 따라 데이터를 정렬해보자.
    • https://jsonplaceholder.typicode.com/users?sortOrder=name or email
  • fast-sort 라이브러리를 사용하여 데이터를 정렬할 예정이므로, 라이브러리를 설치한다.
npm install fast-sort
  • 이전에 작성했던 app/users/UserTable.tsx 파일을 수정하여 쿼리스트링 기반의 정렬을 적용해보자.
    • 테이블 헤더 부분에 사용자가 정렬 방식을 선택할 수 있는 링크 태그를 추가한다.
import React from "react";
import Link from "next/link";

interface User {
  id: number;
  name: string;
  email: string;
}

const UserTable = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users: User[] = await res.json();

  return (
    <table>
      <thead>
        <tr>
          <th>
            <Link href="/users?sortOrder=name">Name</Link>
          </th>
          <th>
            <Link href="/users?sortOrder=email">Email</Link>
          </th>
        </tr>
      </thead>
      <tbody>
        {users.map((user) => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>{user.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default UserTable;

📌 a 태그를 사용하지 않고 Link Component를 사용하는 이유

  • a 태그를 사용하면 해당 링크를 클릭할 때마다 연결된 페이지를 방문하기 위해 필요한 모든 자원을 다시 불러와야 하므로 효율성이 떨어질 수 있음
  • Link Component
    • Next.js의 Link 컴포넌트를 사용하면 클라이언트측 라우팅을 활용하여 페이지 간 전환이 이루어짐
    • 이로 인해 페이지가 새로 렌더링되지 않고 필요한 부분만 업데이트되어 더 빠르고 부드러운 사용자 경험을 제공
  • 이제 부모 컴포넌트 propssearchPrams 키워드를 통해 sortOrder 쿼리스트링에 접근하고, 자식 컴포넌트로 sortOrder 값을 전달한다.

app/users/page.tsx

import React from "react";
import UserTable from "./UserTable";

interface Props {
  searchParams: { sortOrder: string };
}

const UsersPage = ({ searchParams: { sortOrder } }: Props) => {
  return (
    <div>
      <div>This is UserPage</div>
      <p>{new Date().toLocaleTimeString()}</p>
      <UserTable sortOrder={sortOrder} />
    </div>
  );
};

export default UsersPage;
  • 자식 컴포넌트에서는 props로 전달받은 sortOrder 값에 따라 users 배열을 정렬한다.
    • fast-sort 모듈을 사용하여, 기본 정렬 값을 name으로 하고, sortOrderemail 일 경우 email을 기준으로 정렬하는 sortUsers 함수를 작성

app/users/UserTable.tsx

import React from "react";
import Link from "next/link";
import { sort } from "fast-sort";

interface User {
  id: number;
  name: string;
  email: string;
}

interface Props {
  sortOrder: string;
}

const UserTable = async ({ sortOrder }: Props) => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users: User[] = await res.json();

  const sortedUsers = sort(users).asc(
    sortOrder === "email" ? (user) => user.email : (user) => user.name
  );

  return (
    <table>
      <thead>
        <tr>
          <th>
            <Link href="/users?sortOrder=name">Name</Link>
          </th>
          <th>
            <Link href="/users?sortOrder=email">Email</Link>
          </th>
        </tr>
      </thead>
      <tbody>
        {sortedUsers.map((user) => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>{user.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default UserTable;
  • 결과 확인
    • 이제 사용자가 원하는 정렬방식을 선택할 수 있다!

/users?sortOrder=name

/users?sortOrder=email

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글