# [Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기

[Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기

2편에서 프로젝트를 띄웠다면, 이번 편은 타입을 붙인 컴포넌트를 만듭니다.
목표: Memo 타입 정의 → MemoCard props 타입 → 홈에 샘플 메모 보여 주기.


이 편에서 할 일

  1. 메모 데이터 모델(Memo) 타입 만들기
  2. props에 타입이 있는 MemoCard 컴포넌트 만들기
  3. 홈 페이지에 샘플 데이터로 카드 렌더하기

고급 타입(제네릭 마법, utility type 잔치)은 하지 않습니다.
props / 데이터 모델만 확실하게 잡습니다.


왜 컴포넌트에 타입을 붙이나

React만 쓸 때도 props는 넘어갑니다.
문제는 “이 컴포넌트가 뭘 받는지”가 코드만 봐서는 흐릿하다는 점입니다.

// 타입 없이
function MemoCard({ title, content }) {
  return <article>...</article>;
}
  • title이 string인지?
  • createdAt도 넘겨야 하는지?
  • 오타로 titel을 넘기면?

TypeScript를 붙이면 받는 쪽과 주는 쪽이 같은 계약을 공유합니다.


1) 데이터 모델부터: Memo 타입

프로젝트에 타입 전용 파일을 하나 둡니다.

경로: types/memo.ts

export type Memo = {
  id: string;
  title: string;
  content: string;
  createdAt: string;
};

포인트:

  • type으로 객체의 모양을 고정한다
  • 나중에 API/DB에서 같은 형태를 쓰면 한곳에서만 바꾸면 된다
  • idnumber로 할지 string으로 할지는 팀마다 다르지만, 이 시리즈는 string으로 통일한다

2) props 타입: MemoCard

컴포넌트 폴더를 만듭니다.

경로: components/MemoCard.tsx

import type { Memo } from "@/types/memo";

type MemoCardProps = {
  memo: Memo;
};

export default function MemoCard({ memo }: MemoCardProps) {
  return (
    <article>
      <h2>{memo.title}</h2>
      <p>{memo.content}</p>
      <time dateTime={memo.createdAt}>{memo.createdAt}</time>
    </article>
  );
}

읽어볼 것:

코드 의미
import type 타입만 가져옴 (런타임 코드 아님)
MemoCardProps 이 컴포넌트가 받는 props 계약
{ memo }: MemoCardProps 구조 분해 + 타입 적용
memo.title Memo에 있는 필드만 자동완성됨

실수로 memo.titlle을 쓰면 에디터/타입이 바로 에러를 냅니다.
1편에서 말했던 “안전벨트”가 여기서 체감됩니다.


3) 홈에서 샘플 데이터로 연결

app/page.tsx를 아래처럼 바꿉니다.

import MemoCard from "@/components/MemoCard";
import type { Memo } from "@/types/memo";

const sampleMemos: Memo[] = [
  {
    id: "1",
    title: "Next.js 시작",
    content: "App Router로 프로젝트 만들었다.",
    createdAt: "2026-07-29",
  },
  {
    id: "2",
    title: "TypeScript props",
    content: "MemoCard에 타입을 붙였다.",
    createdAt: "2026-07-29",
  },
];

export default function HomePage() {
  return (
    <main>
      <h1>Memo App</h1>
      <p>샘플 메모 목록</p>

      <section>
        {sampleMemos.map((memo) => (
          <MemoCard key={memo.id} memo={memo} />
        ))}
      </section>
    </main>
  );
}

확인 포인트:

  • sampleMemos: Memo[] → 배열 원소가 전부 Memo여야 함
  • 필드 하나 빠지거나 이름 틀리면 페이지에서 바로 빨간줄
  • key={memo.id}는 React 리스트 렌더링 기본기 (타입과는 별개, 필수)

npm run devhttp://localhost:3000을 보면 카드 두 개가 보여야 합니다.


props를 펼치는 다른 방식 (참고)

지금은 memo 객체 하나를 통째로 넘겼습니다.

가끔은 필드를 하나씩 받기도 합니다.

type MemoCardProps = {
  title: string;
  content: string;
  createdAt: string;
};

둘 다 틀린 건 아닙니다.

이 시리즈 추천:

데이터가 하나의 모델이면 객체로 넘긴다 (memo: Memo)

필드가 늘어날 때마다 props를 하나씩 추가하는 것보다,
모델만 고치면 되어서 유지보수가 쉽습니다.


@/ 는 뭐지?

create-next-app에서 import alias를 켰다면:

import type { Memo } from "@/types/memo";

@/는 보통 프로젝트 루트를 가리킵니다.
tsconfig.jsonpaths에 정의되어 있습니다.

상대 경로 지옥(../../../types/memo)을 피하려고 쓰는 설정입니다.


자주 하는 실수

1. Memo를 값처럼 import
→ 타입만 쓸 때는 import type { Memo } 권장

2. props 타입을 안 쓰고 매개변수만 적음
({ memo })만 쓰면 memoany에 가까워져 이득이 사라짐

3. key를 인덱스(index)로만 씀
→ 샘플에선 당장 안 터져도, 목록이 바뀌면 버그 나기 쉬움. id 사용

4. types 폴더 위치 착각
app/types가 아니라 프로젝트 루트의 types/ (이 시리즈 기준)


이 편에서 가져갈 것

  • 데이터 모양은 type으로 먼저 고정한다
  • 컴포넌트 props에도 같은 타입을 연결한다
  • Memo[]로 샘플을 만들면 잘못된 데이터가 즉시 드러난다
  • 모델 하나면 props도 객체 하나로 넘기는 편이 단순하다

다음 편 예고

[Next.js + TS] 4. 라우팅과 레이아웃

  • /memos 목록 페이지 만들기
  • /memos/[id] 상세 페이지
  • layout.tsx에 간단한 내비 넣기

지금은 홈에 샘플이 붙어 있는 상태입니다.
다음 편에서 URL별로 화면을 나누기 시작합니다.


시리즈 목차

  1. 왜 Next.js + TypeScript인가
  2. 프로젝트 만들고 폴더 구조 이해하기
  3. TypeScript로 컴포넌트 타입 잡기 ← 지금
  4. 라우팅과 레이아웃
  5. Server Component vs Client Component
  6. 데이터 가져오기
  7. 폼과 서버 액션 입문
  8. 정리 + 다음 학습 로드맵