프로그래밍/next.js

[커리큘럼 페이지 프로젝트] Next.js App Router에서 화면과 데이터 호출 흐름 이해하기

d 0_0 b 2026. 6. 7. 14:52

 

 

커리큘럼 페이지를 리팩토링 하며 1인 개발에 도전하기로 했다. 그러기 위해선 Next.js를 이해할 필요가 있다.

 

 

가장 궁금했던 부분은 Next.js가 어떤 흐름으로 동작하는지였다. 특히 page.tsx, 클라이언트 컴포넌트, hook, API 함수, route.ts가 각각 어떤 역할을 맡고 있는지 정리할 필요가 있었다.

 

현재 구조는 백엔드를 구축하기 이전에 next만을 이용한 구조입니다.

route.ts가 그 역할을 대신하고 있으니,

백엔드와 프론트엔드의 api호출 흐름까지 보기 위해선 다음 글도 보시면서 이해하면 좋을 것 같습니다.

 

 

 

이 프로젝트의 Next.js 데이터 흐름은 다음과 같이 정리할 수 있다.

app/page.tsx
  -> 서버에서 초기 데이터 생성
  -> MainPageClient에 initialData 전달
  -> MainPageClient가 useCareerList 실행
  -> useCareerList가 items/loading/error 상태 관리
  -> useEffect에서 load 실행
  -> load 안에서 fetchCareerList 호출
  -> fetchCareerList가 /api/careers/list로 POST 요청
  -> app/api/careers/list/route.ts의 POST 함수 실행
  -> JSON 응답 반환
  -> useCareerList가 setItems로 상태 업데이트
  -> CareerSide가 items를 화면에 렌더링

 

 

전체 흐름을 한 문장으로 정리하면 다음과 같다.

Next.js는 서버에서 초기 화면을 만들고, 브라우저에서 클라이언트 컴포넌트가 상태와 이벤트를 관리하며, 필요한 데이터는 내부 API Route를 통해 다시 호출하는 구조로 동작한다.

 

 

1. 파일 위치가 곧 URL이 된다

Next.js App Router에서는 app 디렉터리 안의 파일 구조가 곧 라우팅 구조가 된다.

예를 들어 다음 파일은 / 경로의 페이지가 된다.

frontend/src/app/page.tsx
export default function Home() {
  return <MainPageClient initialData={createMainMock()} />
}

app/page.tsx는 기본적으로 서버 컴포넌트다. 따라서 브라우저가 아니라 서버에서 먼저 실행된다. 이 코드에서는 서버에서 createMainMock()으로 초기 데이터를 만들고, 그 데이터를 MainPageClient에 initialData라는 prop으로 넘긴다.

즉, 첫 화면에 필요한 기본 데이터는 서버에서 만들어지고, 그 결과가 클라이언트 컴포넌트로 전달된다.

 

 

2. 서버 컴포넌트와 클라이언트 컴포넌트가 나뉜다

Next.js App Router에서는 컴포넌트가 기본적으로 서버 컴포넌트로 동작한다. 반대로 파일 상단에 "use client"가 있으면 해당 컴포넌트는 클라이언트 컴포넌트가 된다.

"use client"

MainPageClient.tsx에는 "use client"가 있기 때문에 브라우저에서 실행된다. 그래서 이 컴포넌트 안에서는 useState, useEffect, useMemo 같은 React hook을 사용할 수 있다.

현재 구조는 다음과 같이 볼 수 있다.

app/page.tsx
  -> 서버에서 초기 데이터 생성
  -> MainPageClient에 initialData 전달
  -> 브라우저에서 상태, 이벤트, 추가 데이터 호출 처리

즉, page.tsx는 초기 화면을 준비하는 역할을 하고, MainPageClient는 사용자가 보는 화면의 상태와 동작을 관리하는 역할을 한다.

 

 

3. app/api 아래의 route.ts는 내부 API 서버처럼 동작한다

다음 파일은 Next.js의 API Route다.

frontend/src/app/api/careers/list/route.ts

파일 경로가 다음과 같기 때문에 실제 URL은 /api/careers/list가 된다.

app/api/careers/list/route.ts
-> /api/careers/list

그리고 이 파일 안에서 POST 함수를 export하면, POST /api/careers/list 요청을 처리할 수 있다.

export async function POST(request: Request) {
  const body = (await request.json().catch(() => ({}))) as CareerListRequest

  const response: CareerListResponse = {
    code: 200,
    status: "OK",
    message: "커리어 사이드 목록 조회 성공",
    data: {
      items: createCareerListMock(body),
    },
  }

  return NextResponse.json(response)
}

브라우저 입장에서는 백엔드 API를 호출하는 것처럼 보이지만, 실제로는 같은 Next.js 프로젝트 안에 있는 route.ts가 요청을 받아 응답을 만들어준다.

 

 

4. MainPageClient는 hook을 통해 데이터를 사용한다

MainPageClient에서는 커리어 목록을 가져오기 위해 useCareerList라는 hook을 사용한다.

const careerList = useCareerList(
  initialData.careerSide.items.map((career) => ({
    careerId: career.id,
    name: career.name,
    category: {
      categoryId: career.categoryId,
      name: career.categoryName,
    },
    displayOrder: career.displayOrder,
  })),
)

여기서 중요한 점은 MainPageClient가 직접 fetch를 호출하지 않는다는 것이다. MainPageClient는 useCareerList를 호출하고, 그 결과로 items, loading, error 같은 상태를 받아 화면에 넘긴다.

<CareerSide
  error={careerList.error}
  items={careerList.items}
  loading={careerList.loading}
/>

즉, MainPageClient는 화면 전체를 조립하는 컴포넌트이고, 실제 데이터 상태 관리는 useCareerList hook이 담당한다.

 

 

5. useCareerList는 데이터 상태를 관리한다

useCareerList는 커리어 목록 데이터를 React 상태로 관리하는 hook이다.

const [items, setItems] = useState<CareerListItem[]>(initialItems)
const [loading, setLoading] = useState(initialItems.length === 0)
const [error, setError] = useState<string | null>(null)

여기서 관리하는 상태는 크게 세 가지다.

items   -> 화면에 보여줄 데이터
loading -> 데이터를 불러오는 중인지 여부
error   -> API 호출 중 에러가 발생했는지 여부

이 hook 안에는 데이터를 다시 불러오는 load() 함수가 있고, 컴포넌트가 브라우저에 처음 렌더링된 뒤 useEffect를 통해 load()가 실행된다.

useEffect(() => {
  void load()
}, [load])

이 부분 때문에 초기 데이터가 이미 있더라도 브라우저에서 다시 한 번 /api/careers/list를 호출하게 된다.

따라서 현재 구조는 다음과 같다.

서버에서 initialData로 첫 화면을 빠르게 그림
-> 브라우저가 실행됨
-> useEffect가 실행됨
-> API를 다시 호출함
-> 응답 결과로 items를 갱신함

처음에는 서버에서 받은 데이터로 화면이 바로 보이고, 이후 클라이언트에서 다시 API를 호출해 최신 상태로 갱신하는 구조라고 볼 수 있다.

 

 

6. fetchCareerList는 실제 API 요청을 보내는 함수다

처음에 헷갈렸던 부분은 useCareerList 다음에 왜 fetchCareerList가 나오는지였다.

정리하면 역할이 다르다.

useCareerList   -> 데이터 상태를 관리하는 hook
fetchCareerList -> 실제 서버에 요청을 보내는 함수

MainPageClient가 직접 fetchCareerList를 부르는 것이 아니다. 흐름은 다음과 같다.

MainPageClient 렌더링
  -> useCareerList 실행
    -> useEffect 실행
      -> load 실행
        -> fetchCareerList 실행

fetchCareerList는 다음과 같이 API를 호출한다.

export async function fetchCareerList(payload = {}) {
  const res = await fetch("/api/careers/list", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  })

  return await res.json()
}

여기서 fetch("/api/careers/list")는 Next.js 내부 API Route를 호출한다. 즉, 이 요청은 다음 파일의 POST 함수로 연결된다.

frontend/src/app/api/careers/list/route.ts

전체 호출 관계를 다시 정리하면 다음과 같다.

MainPageClient
  -> useCareerList
    -> load
      -> fetchCareerList
        -> fetch("/api/careers/list")
          -> app/api/careers/list/route.ts의 POST 함수
            -> JSON 응답
              -> setItems
                -> CareerSide 렌더링

 

 

7. 화면, hook, API 함수, route.ts의 역할 구분

이 구조를 이해하려면 각 파일의 역할을 분리해서 봐야 한다.

page.tsx
-> 서버에서 초기 페이지를 준비한다.

MainPageClient.tsx
-> 브라우저에서 실행되는 화면 컴포넌트다.
-> 상태와 이벤트가 필요한 컴포넌트를 포함한다.

useCareerList
-> 커리어 목록 데이터의 상태를 관리한다.
-> loading, error, items 같은 값을 제공한다.

fetchCareerList
-> 실제 API 요청을 보낸다.
-> fetch("/api/careers/list")를 실행한다.

route.ts
-> API 요청을 받는다.
-> 데이터를 만들고 JSON으로 응답한다.

CareerSide
-> 전달받은 데이터를 화면에 렌더링한다.

이렇게 보면 useCareerList와 fetchCareerList의 차이가 분명해진다. useCareerList는 상태 관리의 관점이고, fetchCareerList는 네트워크 요청의 관점이다.

 

 

8. Link는 Next.js 라우팅을 사용한다

커리어 항목을 클릭할 때는 next/link를 사용한다.

<Link href={`/careers/${item.careerId}`}>

예를 들어 careerId가 1이면 /careers/1로 이동한다. 이 URL은 다음 파일이 처리한다.

app/careers/[careerId]/page.tsx

여기서 [careerId]는 동적 라우트다. 대괄호로 감싼 폴더 이름은 URL 파라미터가 된다.

/careers/1
/careers/2
/careers/3

이런 URL들이 모두 [careerId]에 매핑된다.

 

 

9. 이 코드의 전체 데이터 흐름

이 프로젝트의 데이터 흐름은 다음과 같이 정리할 수 있다.

1. 사용자가 / 페이지에 접속한다.

2. app/page.tsx가 서버에서 실행된다.

3. createMainMock()으로 초기 데이터를 만든다.

4. MainPageClient에 initialData를 전달한다.

5. MainPageClient가 브라우저에서 실행된다.

6. useCareerList가 초기 데이터를 상태에 넣는다.

7. useEffect가 실행되면서 load()를 호출한다.

8. load() 안에서 fetchCareerList가 실행된다.

9. fetchCareerList가 /api/careers/list로 POST 요청을 보낸다.

10. app/api/careers/list/route.ts의 POST 함수가 요청을 처리한다.

11. JSON 응답이 돌아온다.

12. useCareerList가 setItems로 상태를 갱신한다.

13. CareerSide가 갱신된 items를 화면에 렌더링한다.

 

 

10. 이번에 이해한 핵심

이번 코드를 통해 이해한 핵심은 Next.js에서 화면과 데이터 호출이 한 곳에 섞여 있는 것이 아니라 역할별로 나뉘어 있다는 점이다.

page.tsx는 서버에서 초기 화면을 준비한다. "use client"가 붙은 컴포넌트는 브라우저에서 상태와 이벤트를 처리한다. hook은 데이터의 로딩, 에러, 결과 상태를 관리한다. API 함수는 실제 요청을 보낸다. route.ts는 그 요청을 받아 응답을 만든다.

처음에는 useCareerList 다음에 왜 fetchCareerList가 이어지는지 헷갈렸지만, 지금은 둘의 역할을 구분해서 이해할 수 있다.

useCareerList는 데이터를 관리하는 사람이고,
fetchCareerList는 서버에 실제로 요청을 보내는 사람이다.

그래서 이 프로젝트의 Next.js 구조는 다음과 같이 요약할 수 있다.

서버에서 초기 화면을 만들고,
클라이언트에서 상태와 이벤트를 관리하며,
필요한 데이터는 내부 API Route를 통해 다시 호출하는 구조

이 흐름을 이해하면 Next.js 프로젝트에서 page.tsx, 클라이언트 컴포넌트, custom hook, API 함수, route.ts가 각각 왜 필요한지 더 명확하게 볼 수 있다.