logo

app 디렉토리

페이지, 레이아웃, API

79 조회

0 추천

1,691 단어

9분 예상

2025. 01. 11. 게시

2025. 02. 26. 수정

luasenvy 작성

CC BY-NC-SA 4.0

app 디렉토리

nextjs 구조에서 사전 정의된 디렉토리로 화면, API와 같이 클라이언트가 접근하고 사용할 수 있는 실질적인 어플리케이션기능들이 위치한다.

페이지

AppRouter 구조에서 페이지를 만들기 위해서는 app 디렉토리 하위에 page.tsx 파일을 만듦으로써 구성할 수 있다. app 디렉토리를 루트 디렉토리로 하여 page.tsx가 위치한 곳까지의 디렉토리 경로를 URL로 치환하여 라우트 경로가 결정된다. 설치 이후에 접속하여 확인한 인덱스 페이지는 ./app/page.tsx가 화면에 로드된 결과이다.

// ./app/first/page.tsx
export default function FirstPage(){
  return <div>
    Hello World
  </div>
}

위 처럼 파일을 만들고 저장하게 되면 localhost:3000/first로 접속하여 확인할 수 있다.

경로 파라미터와 쿼리스트링

app/first
├── [someId]
│   └── page.tsx
└── page.tsx

경로 파라미터를 사용하기 위해서 디렉토리를 [variable_name] 형식으로 지정할 수 있다. page.tsx에서는 SSR 방식에 따라 참조할 수 있는 방법이 다르다.

interface FirstParamsPageProps {
  params: Promise<{ someId: string }>
  searchParams: Promise<{ someName: string }>
}

export default async function FirstPage({ params, searchParams }: FirstParamsPageProps){
  const { someId } = await params;
  const { someName } = await searchParams;

  return <div>
    {someId} - {someName}
  </div>
}

SSR방식으로 로드할 경우 페이지 함수가 async로 구성되어야한다. params, searchParams의 이름으로 넘어오는 파라미터를 통해 사용할 수 있으며 사용전에 반드시 await 하여야 한다.

"use client";

import { useParams, useSearchParams } from "next/navigation";

export default function FirstPage(){
  const { someId } = useParams<{ someId: string }>()
  const searchParams = useSearchParams()
  
  return <div>
    {someId} - {searchParams.get("someName")}
  </div>
}

SSR을 사용하지 않을 경우에는 next/navigation 패키지에 있는 useParams, useSearchParams 훅을 활용하여 사용할 수 있다.

app/first
├── else
│   └── [...params]
│       └── page.tsx
├── [someId]
│   └── page.tsx
└── page.tsx

경로 파라미터로 사용할 변수명 앞에 ... 을 함께 사용하면 하위 모든 형태와 깊이의 경로를 포함하여 경로 파라미터로 사용할 수 있다. 위 경우에는 /app/first/else/*/*에 해당하는 라우트를 생성할 수 있다.

라우트 영향없이 디렉토리 나누기

app/first
├── (new)
│   └── help
│       └── page.tsx
├── [someId]
│   └── page.tsx
└── page.tsx

라우트 경로에 영향을 주지 않고 디렉토리를 분리하고 싶을 때에는 (dirname) 과 같이 괄호를 감싼 디렉토리 명으로 사용할 수 있다. 위 처럼 구성한다면 localhost:3000/first/help로 라우트 경로를 생성할 수 있다. 적용할 레이아웃을 나누거나 관리목적으로 나누는 경우에 유용하게 사용할 수 있다. 라우트 경로에 영향을 주지 않기 때문에 다른 라우트 경로와 겹치는 경우가 없는지 잘 확인해야한다.

컴포넌트

.
├── app
│   └── first
│       └── page.tsx
├── components
│   └── Counter.tsx

nextjs는 react를 프론트엔드 프레임워크로 사용하고 있다. 리액트용 컴포넌트를 관례적으로 components라는 명칭의 디렉토리에서 관리하는 것이 일반적이나 app과 같이 특별한 기능이 지원되는 디렉토리가 아니다.

// components/Counter.tsx

"use client";

import { useState } from "react";

export default function Counter () {
  const [count, setCount] = useState<number>(0);

  const handleClickButton = () => setCount((prev) => prev + 1);

  return <button type="button" onClick={handleClickButton}>{count}</button>
}

app 디렉토리와 같은 위치에 components 디렉토리를 만들고 새로운 리액트 컴포넌트인 Counter.tsx를 작성하였다.

//app/first/page.tsx 

import Counter from "@/components/Counter";

export default function FirstPage(){
  return <div>
    <Counter />
  </div>
}

프로젝트 루트를 뜻하는 @ 를 사용하면 쉽게 컴포넌트가 있는 위치를 지정할 수 있다. 방금 작성한 컴포넌트를 임포트하여 바로 사용할 수 있다.

레이아웃

레이아웃은 페이지를 포함한 화면 구조이다. 웹 어플리케이션의 화면을 구성함에 있어 동적으로 변경되는 페이지를 제외하고 헤더, 푸터, 사이드바와 같이 고정되어 있는 요소를 구성할 때 편리하다. 렌더링 되는 태그 뿐만 아니라 폰트 설정, 전역 CSS와 같이 공용 자원을 미리 불러오는데 사용할 수도 있다.

레이아웃은 루트로 부터 페이지가 존재하는 위치까지의 모든 레이아웃이 순차적으로 로드된다.

여기서, ./app/layout.tsx 는 가장 최상위 레이아웃으로 루트 레이아웃이라고 특별히 지칭하기도 하며 <html> 태그와 <body> 태그를 포함하고 있어야한다.

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

최초 설치시에 자동으로 생성된 루트 레이아웃을 열어보면 위 처럼 구성되어있다. {children} 을 렌더링하는 부분에 페이지 또는 하위 레이아웃이 렌더링 된다.

//./app/first/layout.tsx 

export default function FirstLayout({ children }: React.PropsWithChildren) {
  return <main className="first-layout">
    {children}
  </main>
}

이렇게 first/layout.tsx를 작성하면 first/page.tsx를 로드할 때 위 레이아웃을 포함하여 렌더링 된다. 즉, 루트 레이아웃의 {children}FirstLayout이 렌더링되고, FirstLayout에 작성된 {children}page.tsx가 렌더링되는 식이다.

적용범위

FirstLayout은 ./app/first 디렉토리에 위치하고 있어서 해당 디렉토리의 하위 모든 page.tsx에 적용된다. 예를들어 ./app/first/some/path/page.tsx의 경우도 포함한다. 만약 ./app/second/page.tsx 를 작성하였다면 이 page.tsx에는 영향을 주지 않는다.

API

// ./app/api/hello/route.ts

export async function GET() {
  return Response.json({ hello: "world!" })
}
.
├── app
│   ├── api
│   │   └── hello
│   │       └── route.tsx

app 디렉토리에서 route.ts를 작성하게되면 API로 활용할 수 있다. export 하는 함수의 명칭으로 GET, POST, PUT, DELETE, PATCH 등 restful API를 제공할 수 있다. route.ts는 page.tsx처럼 app 디렉토리 하위 어디에나 위치시킬 수 있고 위치한 곳 까지의 디렉토리 경로가 라우트 경로로 결정된다. 특별히 지정된 디렉토리는 없지만 관례적으로 api 디렉토리 하위에 분리하여 관리하는 것이 일반적이다.

// ./app/first/page.tsx

"use client";

import Counter from "@/components/Counter";
import { useEffect } from "react";

export default function FirstPage() {
  useEffect(() => {
    (async () => {
      const res = await fetch("/api/hello");

      if (!res.ok) throw new Error("Failed to fetch");
      
      const data = await res.json();
      
      console.log(data);
    })()
  }, []);

  return <div>
    <Counter />
  </div>
}

작성한 API 경로를 fetch로 호출하여 테스트 해볼 수 있다.