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로 호출하여 테스트 해볼 수 있다.