본문 바로가기

Timo

Next.js 훑어보기

[React랑 뭐가 다르지?]

<데이터 fetching>

React에서는...

// React (CSR)
const [users, setUsers] = useState([]);

useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(data => setUsers(data));
}, []);

브라우저에서만 실행되는 useEffect와 useState.. 

사용자가 페이지에 들어오고 나서야 데이터 요청이 시작되므로 처음 화면이 뜰 때는 항상 빈 상태이다. 

 

Next.js App Router에서는 2가지 선택지가 있다. 

1. Server Component에서 fetch (초기데이터, SEO 필요할 때)

// app/home/page.tsx — 서버에서 실행됨
export default async function HomePage() {
  const res = await fetch('https://api.timo.com/tasks');
  const tasks = await res.json();

  return <TaskList tasks={tasks} />;
}

async/await을 컴포넌트에서 바로 쓸 수 있음. (HTML 만들기 전에 서버에서 기다릴 수 있어서)

 

  • 서버 컴포넌트: 서버에서 렌더링하므로 컴포넌트 자체를 async로 만들고 await를 바로 사용할 수 있습니다.
  • 클라이언트 컴포넌트: 브라우저에서 렌더링되므로 컴포넌트는 동기적으로 JSX를 반환해야 해서 async 컴포넌트를 사용할 수 없고, 데이터 요청은 보통 useEffect나 라이브러리(예: TanStack Query)를 통해 처리합니다.
    컴포넌트를 async로 만들면 JSX를 반환하는 게 아니라 Promise를 반환. jsx를 반환해야해서.. 못 사용함. 

서버에서 실행되니까 HTML에 이미 데이터가 담긴 채로 브라우저에 도착함. 

 

2. React Query (실시간 갱신, 유저 인터랙션) 

// app/home/_containers/TaskListContainer.tsx
'use client';

import { useQuery } from '@tanstack/react-query';

export const TaskListContainer = () => {
  const { data: tasks } = useQuery({
    queryKey: ['tasks'],
    queryFn: () => fetch('/api/tasks').then(res => res.json()),
  });

  return <TaskList tasks={tasks ?? []} />;
};

목록 갱신, 실시간 업데이트 같은 거 React Query가 담당. 

 

[번들과 서버]

React 앱은 npm run build 하면 HTML, CSS, JS 파일만 나옴.

Next.js는 빌드하면 서버도 같이 생김. (Server Component를 실행하거나, API Routes를 처리하거나, 라우트마다 렌더링 방식을 결정하는 로직이 필요하기 때문)

 

[use client 언제 붙이나]

파일 맨 위에 아무것도 안 쓰면 server 컴포넌트. 

use client 붙인 컴포넌트 하위 트리는 전부 클라이언트 번들에 포함되면서 JS가 브라우저로 전송되는 양이 늘어나므로 남용하면 안됨. → 정말 인터랙션이 필요한 부분만 클라이언트로 내리자. 

우리 프로젝트에서 순수 UI는 _components/, 클라 로직이 있는 건 _containers/로 나눔. 

 

[App Router 예약 파일들]

Next.js에서 app/ 폴더 안에 특정 이름의 파일을 만들면 자동으로 역할이 정해진다. 

page.tsx : 그 URL의 실제 페이지 

layout.tsx : 하위 페이지들을 감싸는 공통 껍데기

우리 프로젝트에서는 QueryProvider로 모든 페이지를 감싸서 어느 페이지에서든 React Query를 쓸 수 있도록 설정했다.
loading.tsx : 로딩 중일 때 보여줄 UI (자동 suspense 처리해줌)

error.tsx : 에러 나면 보여줄 UI

☆ 이 예약 파일들은 export default function을 써야한다!!!

개발자가 아니라 Next.js 프레임워크가 직접 import 해서 쓰기 때문에 export default가 필요. (named export 사용하면 못 찾음)

 

[상태 관리]

어떤 도구를 써야할지 고민이 된다면 

이 상태는 어디에서 온 거지?

생각해봅시다.

 

1. API에서 가져온 데이터라면 React Query 사용. (할 일 목록, 사용자 목록, 게시글 목록 이런거)

♨ zustand에 또 저장하지 말어라.

데이터가 두 군데에 생기게 되니까..

 

2. URL에 남아야 하는 상태라면

searchParams 사용.

(필터 적용 후 링크 공유했을 때 해당 필터로 데이터 보이게 하는 경우)

공유되어야 하는 상태는 URL에 넣습니다.

 

3. 서버와 상관없는 전역 상태라면

Zustand 사용. 

(다크모드, 사이드바 열림, 모달 열림 이런거)

 

4. 입력 폼이라면

React Hook Form 사용.

 

5. 해당 컴포넌트에서만 사용한다면 

useState 사용. 

 

[skill.md 파일]

.agents/skills/
  git/
    timo-commit/SKILL.md     ← 커밋할 때 따르는 규칙
    timo-pr/SKILL.md         ← PR 만들 때 따르는 규칙
  ui/
    timo-page/SKILL.md       ← 페이지 만들 때 따르는 규칙
    timo-component/SKILL.md  ← 컴포넌트 만들 때 따르는 규칙
  quality/
    timo-review/SKILL.md     ← 코드 리뷰할 때 따르는 규칙

각 skill.md 파일 안에는 트리거, 참조 문서, phase별 워크플로우가 적혀있다. 

 

[트리거 키워드]

입력한 말                                    읽는 파일

"커밋해줘", "커밋 만들어줘"         timo-commit/SKILL.md

"페이지 만들어줘"                       timo-page/SKILL.md

"컴포넌트 만들어줘"                    timo-component/SKILL.md

"PR 만들어줘"                            timo-pr/SKILL.md

예: 커밋해줘
Phase 1에서는 변경된 파일을 분석해서 커밋을 어떻게 나눌지 계획표를 보여주고,
사용자 승인을 받아야 Phase 2(실제 커밋 실행)로 넘어가.

 

[_components vs _containers]

_components/ — props만 받아서 그려주는 컴포넌트. 'use client' 없이도 동작

_containers/  useQuery, Zustand store, useState 같은 외부 상태를 직접 건드리는 컴포넌트. (반드시 'use client')

▣ use client 범위 최소화를 위함.