Skip to main content

27장. 목록 화면과 상세 화면 만들기

[5부] 목록 화면과 상세 화면 만들기

[5부] 목록 화면과 상세 화면 만들기

[5부] 목록 화면과 상세 화면 만들기

이제 서비스가 '진짜 앱'처럼 보이기 시작한다

지금까지 우리는 첫 화면을 만들고, 화면을 컴포넌트로 나누고, 입력 폼과 버튼도 만들어 보았다. 여기까지 왔을 때의 화면은 보통 '예쁘게 정리된 소개 페이지'에 가깝다. 하지만 목록 화면과 상세 화면이 생기면 분위기가 확 달라진다. 이제부터는 사용자가 어떤 데이터를 보고, 그중 하나를 눌러 들어가고, 더 자세한 내용을 확인하는 흐름이 생기기 때문이다.

이 변화는 생각보다 크다. 서비스가 단순한 화면 모음이 아니라, 콘텐츠를 다루는 제품처럼 느껴지기 시작한다. 블로그, 커뮤니티, 노트 앱, 상품 목록, 포트폴리오 서비스까지 대부분의 웹 서비스는 결국 "여러 개를 보여주는 화면"과 "하나를 자세히 보여주는 화면"으로 이루어진다.

이번 장에서는 글 목록과 글 상세 페이지를 직접 만들면서, 화면 전환과 데이터 표시의 기본 구조를 익힌다. 아직 백엔드를 만들기 전이므로, 우선은 화면 안에서 사용할 임시 데이터로 시작한다. 이 방식은 아주 중요하다. 초보자가 가장 많이 막히는 이유 중 하나는 모든 것을 한 번에 만들려 하기 때문이다. 하지만 실제로는 화면 구조를 먼저 만들고, 나중에 서버와 연결하는 편이 훨씬 쉽다.

목록 화면과 상세 화면의 역할

목록 화면은 사용자가 전체 그림을 빠르게 훑어보게 해준다. 어떤 글이 있는지, 제목은 무엇인지, 최근 글은 어떤 것인지 한눈에 확인하는 자리다. 반면 상세 화면은 하나의 항목에 집중하게 만든다. 제목, 본문, 작성일, 작성자 같은 정보를 차분하게 보여주는 공간이다.

이 둘은 짝으로 움직인다. 목록만 있으면 내용이 얕고, 상세만 있으면 어디서 들어가야 할지 막막하다. 그래서 대부분의 서비스는 이 흐름을 갖는다.

  • 목록을 보여준다.

  • 사용자가 하나를 고른다.

  • 상세 화면으로 이동한다.

  • 필요하면 다시 목록으로 돌아간다.

이 네 단계만 자연스럽게 연결되어도 서비스는 훨씬 완성도 있어 보인다.

이번 장에서 만들 화면

우리가 만들 서비스는 글을 다루는 작은 웹 서비스다. 따라서 이번 장에서는 두 개의 핵심 화면을 만든다.

첫째는 글 목록 화면이다. 여러 개의 글 카드가 보이고, 각 카드에는 제목, 짧은 요약, 작성일 정도가 들어간다. 사용자는 여기서 읽고 싶은 글을 고른다.

둘째는 글 상세 화면이다. 목록에서 선택한 글의 전체 내용을 보여준다. 제목과 본문이 중심이 되고, 경우에 따라 작성자나 태그 같은 정보도 붙일 수 있다.

지금은 검색 기능이나 정렬 기능까지는 넣지 않는다. 그건 다음 장에서 다룰 내용이다. 이번 장의 핵심은 아주 분명하다. "여러 개를 보여주는 구조"와 "하나를 자세히 보여주는 구조"를 이해하는 것이다.

백엔드가 없을 때는 목업 데이터로 시작하자

아직 서버와 데이터베이스를 만들지 않았으니, 우선은 화면 안에서 직접 데이터를 준비하자. 이런 데이터를 보통 목업 데이터, 더 쉽게 말하면 '가짜 연습용 데이터'라고 생각하면 된다.

예를 들어 data/posts.ts 같은 파일을 하나 만들고, 그 안에 글 목록을 배열로 적어둘 수 있다.

export const posts = [
  {
    id: '1',
    title: '첫 번째 글',
    summary: '서비스의 첫 글입니다.',
    content: '이 글은 상세 페이지에서 전체 내용을 보여주기 위한 예시입니다.',
    author: '민지',
    createdAt: '2026-04-23',
  },
  {
    id: '2',
    title: '두 번째 글',
    summary: '목록과 상세 연결 연습용 글입니다.',
    content: '목록에서 클릭하면 상세 화면으로 이동하게 만들 것입니다.',
    author: '준호',
    createdAt: '2026-04-24',
  },
]

이 데이터는 당장은 손으로 적어 넣은 것이지만, 구조 자체는 나중에 서버에서 받아올 데이터와 거의 비슷하다. 그래서 지금 이 단계에서 목록 화면과 상세 화면을 완성해 두면, 이후 백엔드를 붙일 때 바뀌는 것은 "데이터를 가져오는 방식"이지 "화면 구조" 전체가 아니다.

이게 바로 초보자에게 중요한 감각이다. 화면 문제와 서버 문제를 분리해서 생각해야 한다.

목록 화면부터 만들기

목록 화면은 보통 /posts 같은 주소에 둔다. 홈 화면이 이미 있다면 홈에서 최신 글 몇 개만 보여주고, 전체 목록은 별도의 페이지로 분리해도 좋다. 이번 장에서는 이해를 쉽게 하기 위해 글 목록 페이지를 하나 만든다고 생각하자.

Next.js를 쓰고 있다면 app/posts/page.tsx 파일을 만들 수 있다. 이 페이지에서는 posts 배열을 가져와서 반복 렌더링하면 된다.

import Link from 'next/link'
import { posts } from '@/data/posts'

export default function PostsPage() {
  return (
    <main className="mx-auto max-w-3xl px-4 py-10">
      <h1 className="text-3xl font-bold">글 목록</h1>
      <div className="mt-6 space-y-4">
        {posts.map((post) => (
          <article key={post.id} className="rounded-xl border p-5 shadow-sm">
            <h2 className="text-xl font-semibold">{post.title}</h2>
            <p className="mt-2 text-sm text-gray-600">{post.summary}</p>
            <p className="mt-3 text-xs text-gray-500">
              {post.author} · {post.createdAt}
            </p>
            <Link
              href={`/posts/${post.id}`}
              className="mt-4 inline-block text-sm font-medium text-blue-600"
            >
              자세히 보기
            </Link>
          </article>
        ))}
      </div>
    </main>
  )
}

여기서 중요한 것은 복잡한 디자인이 아니다. 우선은 반복해서 보여주는 흐름을 이해하는 것이 먼저다. posts.map(...)은 배열 안의 글을 하나씩 꺼내 화면에 그려 준다. 그리고 각 글마다 자세히 보기 링크를 붙여 상세 페이지로 이동하게 만든다.

이 순간부터 서비스는 꽤 그럴듯해진다. 사용자는 "여기에 여러 개의 콘텐츠가 있구나"를 바로 이해할 수 있다.

목록 화면에서 꼭 신경 써야 할 것

초보자는 목록 화면을 만들 때 종종 모든 정보를 다 넣으려고 한다. 제목도 넣고, 본문도 길게 넣고, 태그도 넣고, 작성자도 넣고, 버튼도 여러 개 넣는다. 그러면 오히려 화면이 무거워진다.

목록 화면의 핵심은 '비교하기 쉽게 보여주는 것'이다. 그래서 한 항목 안에는 보통 이 정도면 충분하다.

  • 제목

  • 짧은 요약 또는 본문 일부

  • 작성일이나 작성자 같은 보조 정보

  • 상세로 이동하는 링크 또는 클릭 영역

쉽게 말해, 목록 화면은 영화의 예고편 같아야 한다. 전부 다 보여주는 곳이 아니라, 무엇을 볼지 고르게 만드는 곳이다.

카드 하나를 컴포넌트로 분리하면 더 편하다

지난 장에서 컴포넌트로 화면을 나누는 감각을 배웠다면, 이번에도 같은 방식이 큰 도움이 된다. 글 목록 페이지 안에 카드 마크업을 전부 적어도 되지만, PostCard 같은 컴포넌트로 따로 빼 두면 나중에 수정하기가 쉬워진다.

예를 들어 components/PostCard.tsx를 이렇게 만들 수 있다.

import Link from 'next/link'

type PostCardProps = {
  post: {
    id: string
    title: string
    summary: string
    author: string
    createdAt: string
  }
}

export default function PostCard({ post }: PostCardProps) {
  return (
    <article className="rounded-xl border p-5 shadow-sm">
      <h2 className="text-xl font-semibold">{post.title}</h2>
      <p className="mt-2 text-sm text-gray-600">{post.summary}</p>
      <p className="mt-3 text-xs text-gray-500">
        {post.author} · {post.createdAt}
      </p>
      <Link
        href={`/posts/${post.id}`}
        className="mt-4 inline-block text-sm font-medium text-blue-600"
      >
        자세히 보기
      </Link>
    </article>
  )
}

그러면 목록 페이지는 훨씬 짧고 읽기 쉬워진다.

import { posts } from '@/data/posts'
import PostCard from '@/components/PostCard'

export default function PostsPage() {
  return (
    <main className="mx-auto max-w-3xl px-4 py-10">
      <h1 className="text-3xl font-bold">글 목록</h1>
      <div className="mt-6 space-y-4">
        {posts.map((post) => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>
    </main>
  )
}

이렇게 해 두면 목록 디자인을 바꾸고 싶을 때 PostCard만 수정하면 된다. 서비스가 커질수록 이런 분리가 아주 큰 힘을 발휘한다.

상세 화면은 '하나의 글에 집중하는 페이지'다

이제 사용자가 목록에서 글 하나를 눌렀다고 생각해 보자. 그러면 우리는 그 글 하나에 해당하는 페이지를 보여줘야 한다. Next.js에서는 이런 화면을 동적 라우트로 만들 수 있다. 이름은 조금 어려워 보여도 뜻은 단순하다. 주소의 일부가 바뀌는 페이지라는 뜻이다.

예를 들어 /posts/1, /posts/2, /posts/3처럼 뒤의 값이 달라지면, 폴더 구조로는 app/posts/[id]/page.tsx처럼 만들 수 있다. 여기서 [id]는 바뀌는 값이 들어오는 자리다.

상세 페이지의 기본 예시는 이렇게 생각할 수 있다.

import { posts } from '@/data/posts'
import { notFound } from 'next/navigation'

type PostDetailPageProps = {
  params: {
    id: string
  }
}

export default function PostDetailPage({ params }: PostDetailPageProps) {
  const post = posts.find((item) => item.id === params.id)

  if (!post) {
    notFound()
  }

  return (
    <main className="mx-auto max-w-3xl px-4 py-10">
      <p className="text-sm text-gray-500">
        {post.author} · {post.createdAt}
      </p>
      <h1 className="mt-2 text-3xl font-bold">{post.title}</h1>
      <article className="prose mt-8 max-w-none">
        <p>{post.content}</p>
      </article>
    </main>
  )
}

이 코드는 현재 주소에 들어온 id를 읽고, 그 값과 일치하는 글을 posts 배열에서 찾는다. 찾으면 내용을 보여주고, 찾지 못하면 notFound()로 404 화면을 띄운다. 404는 없는 페이지라는 뜻이다.

초보자에게는 이 흐름이 조금 낯설 수 있지만, 사실 아주 자연스럽다. "주소에서 번호를 읽고, 그 번호에 맞는 데이터를 찾아서 보여준다." 이 한 문장으로 이해하면 된다.

목록에서 상세로 이동하는 순간 벌어지는 일

사용자가 목록에서 자세히 보기를 누르면 무슨 일이 일어날까. 겉으로 보기에는 그냥 페이지가 바뀌는 것 같지만, 구조를 이해하면 훨씬 덜 헷갈린다.

먼저 사용자가 링크를 누른다. 그러면 브라우저 주소가 /posts/1 같은 형태로 바뀐다. Next.js는 그 주소에 맞는 파일, 즉 app/posts/[id]/page.tsx를 찾는다. 그리고 id 값이 1이라는 사실을 페이지에 전달한다. 페이지는 그 id로 데이터를 찾고, 해당 글을 화면에 그린다.

이 과정을 이해하면 앞으로 서버에서 데이터를 받아올 때도 덜 무섭다. 지금은 배열에서 찾고 있지만, 나중에는 데이터베이스에서 찾게 될 뿐이다. 기본 흐름은 똑같다.

상세 화면에서 너무 많은 욕심을 내지 말자

상세 화면은 한 글을 자세히 보여주는 곳이지만, 그렇다고 이것저것 다 넣을 필요는 없다. 지금 단계에서는 다음 정도면 충분하다.

  • 제목

  • 작성자

  • 작성일

  • 본문

  • 목록으로 돌아가는 링크

여기에 수정 버튼이나 삭제 버튼, 댓글, 좋아요까지 한 번에 넣고 싶어질 수 있다. 하지만 아직은 프론트엔드의 기본 구조를 익히는 단계다. 욕심을 줄일수록 흐름이 선명해진다.

오히려 중요한 것은 읽기 쉬운 배치다. 제목은 충분히 크게, 본문은 줄 간격을 넉넉하게, 보조 정보는 과하게 튀지 않게 두는 편이 좋다. 사용자가 "어디를 봐야 할지" 자연스럽게 느끼게 만드는 것이 좋은 상세 화면의 핵심이다.

존재하지 않는 글을 만났을 때도 대비해야 한다

실제 서비스에서는 사용자가 이상한 주소로 들어올 수 있다. /posts/9999처럼 없는 글 번호를 직접 입력할 수도 있고, 예전에 있던 글이 삭제되었을 수도 있다. 이럴 때 화면이 깨지면 서비스가 불안해 보인다.

그래서 상세 페이지에서는 "이 데이터가 실제로 있는가?"를 꼭 확인해야 한다. 앞의 예시에서 notFound()를 쓴 이유가 바로 이것이다. 없는 글이면 없는 글이라고 보여주는 편이 훨씬 낫다.

이 감각은 앞으로 계속 중요하다. 개발은 결국 정상 상황만 다루는 일이 아니라, 예상 밖의 입력에도 침착하게 대응하는 일이다.

목록과 상세를 연결할 때 자주 생기는 실수

이 단계에서 초보자가 자주 하는 실수는 몇 가지로 정리된다.

첫째, id 값의 타입이 맞지 않는 경우다. 어떤 데이터에서는 숫자처럼 보이지만 실제로는 문자열일 수 있다. 예를 들어 1과 '1'은 겉보기엔 비슷해도 코드에서는 다르게 취급될 수 있다. 그래서 처음에는 아예 문자열로 통일하는 편이 덜 헷갈린다.

둘째, 링크 주소와 폴더 구조가 맞지 않는 경우다. 링크는 /posts/${post.id}로 만들었는데, 실제 폴더는 app/post/[id]처럼 단수형으로 되어 있으면 이동이 되지 않는다. 이런 문제는 의외로 아주 자주 생긴다.

셋째, 목록에는 있는 정보가 상세에는 없는 경우다. 예를 들어 목록 카드에서는 summary만 보여서 잘 보이는데, 상세 페이지에 들어가 보니 content가 빠져 있어서 화면이 허전해질 수 있다. 그래서 처음부터 데이터 구조를 조금 넉넉하게 만들어 두는 편이 좋다.

넷째, 목록에서 카드 전체를 눌러야 하는지, 버튼만 눌러야 하는지가 애매한 경우다. 작은 링크 하나만 달아 놓으면 사용성이 떨어질 수 있다. 가능하다면 카드 전체를 클릭 가능하게 만들거나, 적어도 클릭할 수 있다는 힌트를 충분히 주는 것이 좋다.

AI에게 이렇게 시키면 된다

이번 장은 AI 코딩 도구와 함께 작업하기에 아주 좋은 단계다. 이유는 목표가 분명하기 때문이다. "글 목록을 보여주고, 각 글을 클릭하면 상세 페이지로 이동하게 해 달라." 이 요청은 비교적 작은 단위이고, 결과도 화면에서 바로 확인할 수 있다.

예를 들어 이런 식으로 요청할 수 있다.

Next.js App Router 기준으로 글 목록 페이지와 글 상세 페이지를 만들어줘.
아직 백엔드는 없으니 data/posts.ts에 목업 데이터를 만들고,
/posts 에서 목록을 보여주고,
/posts/[id] 에서 해당 글 상세를 보여줘.
카드 UI는 단순하게 하고, 없는 id로 들어오면 notFound 처리해줘.
파일 단위로 나눠서 제안해줘.

좋은 요청의 특징은 세세한 구현보다 목적과 조건이 분명하다는 점이다. AI가 코드를 제안하면, 우리는 그 결과를 그대로 믿기보다 다음을 확인해야 한다.

  • 폴더 구조가 맞는지

  • 링크 주소가 맞는지

  • 실제로 목록에서 상세로 이동하는지

  • 없는 주소에서 에러가 아니라 안내 화면이 나오는지

AI는 속도를 높여 주지만, 방향을 정하고 확인하는 역할은 결국 우리가 해야 한다.

화면이 단순해도 구조가 좋으면 강하다

초보자는 종종 디자인이 화려해야 서비스가 좋아 보인다고 생각한다. 물론 보기 좋은 화면은 중요하다. 하지만 지금 단계에서 더 중요한 것은 구조다. 목록과 상세가 자연스럽게 연결되고, 데이터가 일관되게 보이고, 페이지 이동이 매끄럽게 되면 이미 서비스의 뼈대는 제대로 세워진 것이다.

반대로 겉모습은 멋져도 구조가 흔들리면 금방 무너진다. 목록에서는 클릭이 되는데 상세가 비어 있거나, 주소는 바뀌는데 내용이 안 보이거나, 없는 글에 들어갔을 때 오류가 터지면 사용자는 바로 불안함을 느낀다.

그러니 이번 장에서는 "멋진 화면"보다 "안정적인 흐름"에 집중하자. 그게 나중에 검색, 정렬, 수정, 삭제, 로그인 기능까지 붙일 수 있는 발판이 된다.

우리가 지금 만든 것은 앞으로 계속 재사용된다

이번 장의 내용을 단지 글 서비스에만 쓰는 기술이라고 생각하면 아깝다. 목록과 상세의 조합은 거의 모든 서비스에서 반복된다.

블로그에서는 글 목록과 글 상세가 되고, 쇼핑몰에서는 상품 목록과 상품 상세가 된다. 채용 서비스에서는 공고 목록과 공고 상세가 되고, 포트폴리오 사이트에서는 프로젝트 목록과 프로젝트 상세가 된다. 이름만 바뀔 뿐 구조는 거의 같다.

바로 이 점 때문에 이번 장이 중요하다. 한 번 제대로 이해하면 앞으로 만들 수 있는 서비스의 폭이 크게 넓어진다.

다음 장으로 가기 전, 꼭 확인할 것

이번 장을 마쳤다면 다음 질문에 스스로 답해 보자.

  • 여러 개의 글을 배열로 준비하고 화면에 반복해서 보여줄 수 있는가?

  • 각 글에서 상세 페이지로 이동하는 링크를 만들 수 있는가?

  • 주소의 id 값을 읽어서 해당 데이터 하나를 찾아 보여줄 수 있는가?

  • 없는 id일 때도 당황하지 않고 처리할 수 있는가?

이 네 가지가 손에 잡히면 아주 잘 따라오고 있는 것이다.

마무리

목록 화면과 상세 화면은 서비스가 본격적으로 살아 움직이기 시작하는 지점이다. 이제 사용자는 단순히 첫 화면을 구경하는 사람이 아니라, 콘텐츠를 탐색하고 선택하고 읽는 사람이 된다. 이 변화는 작아 보여도 서비스 경험의 핵심에 가깝다.

그리고 무엇보다 중요한 점이 있다. 우리는 아직 백엔드를 만들지 않았지만, 이미 서비스의 중요한 흐름을 눈으로 확인했다는 것이다. 이 경험이 쌓이면 개발은 더 이상 막연한 일이 아니다. 화면을 하나씩 만들고, 흐름을 하나씩 연결하고, 나중에 서버를 붙이면 된다는 감각이 생긴다.

다음 장에서는 여기에 한 걸음 더 나아가, 목록이 단순히 나열되는 수준을 넘어서 사용자가 원하는 내용을 더 쉽게 찾도록 검색과 정렬 기능을 붙여 볼 것이다. 이제 우리의 서비스는 점점 더 '쓸 만한 앱'의 모습으로 가까워진다.

책 목록

바이브 코딩으로 완성하는 첫 풀스택 웹 서비스

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기