28장. 검색과 정렬 붙이기
![[5부] 검색과 정렬 붙이기](https://images.tilnote.io/pages/9cd53b6b-ccd0-4c72-917a-92ec667daa0e.webp)
![[5부] 검색과 정렬 붙이기](https://images.tilnote.io/pages/434328fa-52ff-443c-9009-30267bf74ade.webp)
![[5부] 검색과 정렬 붙이기](https://images.tilnote.io/pages/531dabe8-6dee-42f3-bc71-f4954c88870d.webp)
목록이 생겼다면, 이제는 '찾기 쉽게' 만들어야 한다
지난 장에서 우리는 목록 화면과 상세 화면을 만들었다. 이제 사용자는 여러 글을 훑어보고, 하나를 눌러 자세히 읽을 수 있다. 여기까지 와도 서비스의 기본 뼈대는 충분히 갖춰졌다.
하지만 목록이 조금만 길어지면 곧 이런 생각이 든다.
"원하는 글을 한 번에 찾을 수 없을까?"
"최신 글부터 보고 싶은데 순서를 바꿀 수 없을까?"
바로 이 지점에서 검색과 정렬이 등장한다. 이 두 기능은 보기에는 작지만 체감은 꽤 크다. 검색창 하나만 있어도 사용자는 서비스가 훨씬 똑똑해졌다고 느낀다. 정렬 메뉴 하나만 있어도 "아, 내가 원하는 방식으로 볼 수 있네"라는 감각이 생긴다.
이번 장에서는 글 목록 화면에 기본 검색과 정렬 기능을 붙인다. 아직 백엔드를 만들기 전이므로, 지난 장에서 사용한 목업 데이터를 그대로 활용한다. 핵심은 어렵지 않다. 입력값에 따라 목록을 걸러내고, 사용자가 고른 기준에 따라 순서를 바꾸는 것이다.
검색과 정렬은 무엇이 다를까
처음 배우는 사람은 검색과 정렬을 같은 기능처럼 느끼기도 한다. 둘 다 목록을 바꾸기 때문이다. 하지만 역할은 분명히 다르다.
검색은 범위를 줄이는 일이다. 사용자가 입력한 단어와 관련 있는 항목만 남긴다. 예를 들어 제목에 "AI"가 들어간 글만 보여주는 식이다.
정렬은 순서를 바꾸는 일이다. 항목을 없애지는 않고, 보여주는 순서만 바꾼다. 예를 들어 최신순, 오래된순, 제목순처럼 바꿀 수 있다.
쉽게 말하면 검색은 서랍에서 물건을 찾는 일이고, 정렬은 꺼낸 물건을 줄 세우는 일이다. 둘을 함께 쓰면 탐색 경험이 훨씬 좋아진다.
이번 장에서 만들 것
이번 장에서는 글 목록 페이지에 세 가지를 추가한다.
검색어를 입력할 수 있는 검색창
최신순, 오래된순, 제목순 정도의 기본 정렬 메뉴
검색 결과가 없을 때 보여주는 간단한 안내 문구
이 정도만 있어도 서비스는 갑자기 '쓸 만한 앱'처럼 보이기 시작한다. 사용자가 단순히 주어진 목록을 보는 사람이 아니라, 스스로 원하는 방식으로 내용을 탐색하는 사람이 되기 때문이다.
먼저, 어떤 기준으로 검색할지 정하자
검색 기능을 만들기 전에 가장 먼저 정해야 할 것은 "무엇을 검색할 것인가"다. 초보자는 이 질문을 건너뛰고 바로 코드부터 쓰는 경우가 많다. 하지만 방향을 먼저 정하면 구현이 훨씬 쉬워진다.
우리의 작은 글 서비스에서는 제목만 검색해도 충분할 수 있다. 하지만 보통은 제목만 검색하면 아쉬운 경우가 많다. 사용자는 제목은 기억이 안 나지만 요약 문장이나 작성자 이름은 기억할 수도 있기 때문이다.
그래서 입문 단계에서는 제목, 요약, 작성자 정도를 함께 검색 대상으로 두면 좋다. 너무 복잡하지 않으면서도 체감이 좋아진다.
예를 들어 사용자가 "민지"라고 입력했을 때 민지가 쓴 글이 보이고, "배포"라고 입력했을 때 제목이나 요약에 배포가 들어간 글이 보이면 꽤 자연스럽다.
정렬 기준도 욕심내지 말고 작게 시작하자
정렬도 마찬가지다. 처음부터 좋아요순, 댓글순, 조회수순, 인기순, 추천순까지 다 넣고 싶어질 수 있다. 하지만 지금은 프론트엔드 입문 단계다. 기준이 많아질수록 화면도 복잡해지고, 데이터도 더 필요해진다.
이번 장에서는 가장 기본적인 세 가지면 충분하다.
최신순
오래된순
제목순
최신순은 대부분의 콘텐츠 서비스에서 기본값으로 쓰기 좋다. 오래된순은 시간 흐름을 처음부터 보고 싶은 사람에게 도움이 된다. 제목순은 간단하지만 정렬 기능의 개념을 익히기에 아주 좋다.
인터랙션이 생기면 클라이언트 컴포넌트가 필요하다
검색창에 글자를 입력하면 목록이 바로 바뀌어야 하고, 정렬 메뉴를 바꾸면 순서도 즉시 달라져야 한다. 이런 기능은 사용자의 입력에 따라 화면이 실시간으로 바뀌는 인터랙션이다.
Next.js에서는 이런 동작을 만들 때 보통 클라이언트 컴포넌트를 사용한다. 이름이 조금 어려워 보여도 뜻은 단순하다. 브라우저에서 직접 반응하는 컴포넌트라고 이해하면 된다.
지난 장에서 만든 PostCard는 그대로 두고, 이번에는 목록을 탐색하는 전용 컴포넌트를 하나 만드는 방식이 깔끔하다. 예를 들어 PostListExplorer 같은 이름을 붙일 수 있다.
검색과 정렬 전용 컴포넌트 만들기
먼저 components/PostListExplorer.tsx 파일을 만든다고 생각해 보자.
'use client'
import { useMemo, useState } from 'react'
import { posts } from '@/data/posts'
import PostCard from '@/components/PostCard'
type SortOption = 'latest' | 'oldest' | 'title'
function sortPosts(items: typeof posts, sortBy: SortOption) {
const copied = [...items]
if (sortBy === 'latest') {
return copied.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
}
if (sortBy === 'oldest') {
return copied.sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
}
return copied.sort((a, b) => a.title.localeCompare(b.title))
}
export default function PostListExplorer() {
const [keyword, setKeyword] = useState('')
const [sortBy, setSortBy] = useState<SortOption>('latest')
const visiblePosts = useMemo(() => {
const trimmedKeyword = keyword.trim().toLowerCase()
const filtered = posts.filter((post) => {
const target = `${post.title} ${post.summary} ${post.author}`.toLowerCase()
if (!trimmedKeyword) {
return true
}
return target.includes(trimmedKeyword)
})
return sortPosts(filtered, sortBy)
}, [keyword, sortBy])
return (
<section className="mt-6">
<div className="flex flex-col gap-3 rounded-xl border p-4 md:flex-row md:items-center md:justify-between">
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="제목, 요약, 작성자로 검색해 보세요"
className="w-full rounded-lg border px-4 py-2 text-sm"
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as SortOption)}
className="rounded-lg border px-4 py-2 text-sm"
>
<option value="latest">최신순</option>
<option value="oldest">오래된순</option>
<option value="title">제목순</option>
</select>
</div>
<p className="mt-4 text-sm text-gray-500">
총 {visiblePosts.length}개의 글
</p>
{visiblePosts.length === 0 ? (
<div className="mt-6 rounded-xl border border-dashed p-8 text-center text-sm text-gray-500">
검색 결과가 없어요. 다른 단어로 다시 찾아보세요.
</div>
) : (
<div className="mt-6 space-y-4">
{visiblePosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
)}
</section>
)
}코드가 조금 길어 보이지만, 실제로 하는 일은 아주 단순하다. 입력값을 상태로 저장하고, 그 값에 따라 목록을 필터링한 뒤, 정렬 기준에 맞게 다시 줄 세우는 것이다.
페이지에서는 이 컴포넌트를 불러오기만 하면 된다
이제 app/posts/page.tsx는 훨씬 깔끔해질 수 있다.
import PostListExplorer from '@/components/PostListExplorer'
export default function PostsPage() {
return (
<main className="mx-auto max-w-3xl px-4 py-10">
<h1 className="text-3xl font-bold">글 목록</h1>
<p className="mt-2 text-sm text-gray-600">
원하는 글을 검색하고, 보기 편한 순서로 정렬해 보세요.
</p>
<PostListExplorer />
</main>
)
}이 구조의 장점은 분명하다. 페이지는 페이지 역할에 집중하고, 검색과 정렬 같은 상호작용 로직은 별도 컴포넌트가 맡는다. 파일이 길어지지 않고, 나중에 기능을 고치기도 쉬워진다.
이 코드에서 꼭 이해해야 할 핵심
이 장에서 진짜 중요한 것은 코드를 그대로 외우는 것이 아니라, 흐름을 이해하는 것이다.
첫째, keyword와 sortBy는 사용자의 선택을 기억하는 값이다. 검색창에 입력한 글자와 드롭다운에서 고른 정렬 기준이 여기에 들어간다.
둘째, filter()는 검색에 맞는 글만 남긴다. 배열 전체를 돌면서 조건에 맞는 항목만 통과시키는 방식이다. 사용자가 아무것도 입력하지 않았다면 모든 글을 그대로 보여준다.
셋째, sort()는 화면에 보여줄 순서를 바꾼다. 최신순이면 날짜가 큰 것부터 앞으로 오고, 오래된순이면 반대로 정렬된다.
넷째, useMemo()는 결과를 계산해 두는 도구다. 지금 단계에서는 "검색어나 정렬 기준이 바뀔 때만 다시 계산한다" 정도로 이해해도 충분하다. 꼭 외울 필요는 없지만, 목록을 매번 새로 계산하는 흐름을 정리할 때 자주 쓰인다.
왜 sort() 전에 배열을 복사할까
코드를 보다 보면 const copied = [...items]라는 줄이 눈에 띈다. 이게 왜 필요할까.
자바스크립트의 sort()는 원래 배열 자체의 순서를 바꿔 버린다. 즉, 조심하지 않으면 원본 데이터가 뒤섞일 수 있다. 초보자는 이 부분에서 자주 헷갈린다. 화면에 보여줄 순서만 바꾸고 싶었는데, 원본 자체가 바뀌어 다음 계산에도 영향을 줄 수 있기 때문이다.
그래서 정렬할 때는 먼저 복사본을 만들고, 그 복사본만 정렬하는 습관이 좋다. 이런 작은 습관이 나중에 버그를 꽤 많이 줄여 준다.
검색은 생각보다 '허술해도' 쓸 만하다
처음 검색 기능을 만들면 종종 너무 거창하게 생각한다. 맞춤법도 잡아야 할 것 같고, 띄어쓰기도 자동 보정해야 할 것 같고, 철자가 조금 틀려도 찾아줘야 할 것 같다.
물론 그런 검색은 더 똑똑하다. 하지만 지금 단계에서는 전혀 필요하지 않다. includes()를 사용한 단순 검색만으로도 사용자 경험은 꽤 좋아진다.
예를 들어 제목이 "Vercel로 첫 배포하기"인 글이 있을 때, 사용자가 "배포"라고 입력하면 그 글이 보인다. 이것만으로도 이미 충분히 유용하다. 작은 기능이지만 사용자는 "원하는 걸 빠르게 찾을 수 있다"는 만족을 느낀다.
여기서 중요한 것은 완벽한 검색 엔진을 만드는 것이 아니다. 서비스 탐색을 덜 답답하게 만드는 것이다.
검색 결과가 없을 때도 화면은 친절해야 한다
검색 기능을 붙이면 반드시 생기는 상황이 있다. 바로 결과가 하나도 없을 때다.
초보자는 이 경우를 빼먹기 쉽다. 그러면 화면에는 아무것도 안 보이고, 사용자는 서비스가 고장 났다고 느낄 수 있다. 그래서 결과가 없을 때는 아주 짧은 문장이라도 보여주는 편이 좋다.
"검색 결과가 없어요. 다른 단어로 다시 찾아보세요."
이 한 줄만 있어도 서비스는 훨씬 친절해 보인다. 나중에 7부에서 빈 화면과 실패 화면을 더 자세히 다루겠지만, 지금 단계에서도 최소한의 안내는 해 주는 편이 좋다.
검색창과 정렬 메뉴는 어디에 두는 게 좋을까
이 기능을 붙일 때 의외로 많이 고민하는 부분이 배치다. 어디에 둬야 사용자가 자연스럽게 쓸까?
가장 무난한 방법은 목록 위쪽에 두는 것이다. 사용자는 보통 위에서 아래로 화면을 읽기 때문에, 글을 보기 전에 검색하거나 정렬 기준을 바꿀 수 있으면 흐름이 좋다.
검색창은 넓게 두고, 정렬 메뉴는 그 옆이나 아래에 두면 된다. 모바일에서는 세로로 쌓고, 데스크톱에서는 가로로 배치하면 자연스럽다. 이번 예시에서 md:flex-row를 쓴 이유가 바로 그것이다.
아직 디자인을 과하게 욕심낼 필요는 없다. 중요한 것은 사용자가 기능을 발견하기 쉬운 위치에 두는 것이다.
자주 생기는 실수들
검색과 정렬은 쉬워 보이지만, 실제로 손을 움직여 보면 자주 막히는 지점이 있다.
첫째, 대소문자 문제다. 사용자는 ai라고 입력했는데 데이터에는 AI라고 적혀 있을 수 있다. 이런 차이 때문에 검색이 안 되면 꽤 답답하다. 그래서 보통 검색할 문자열과 입력값을 모두 소문자로 바꿔 비교한다.
둘째, 공백 문제다. 사용자가 실수로 앞이나 뒤에 공백을 넣는 경우가 있다. trim()을 쓰면 이런 불필요한 공백을 제거할 수 있다.
셋째, 날짜 형식 문제다. 정렬 기준에 날짜를 쓰려면 날짜 문자열이 일정한 형식이어야 한다. 2026-04-23처럼 통일해 두면 다루기 편하다. 형식이 뒤섞이면 정렬 결과가 이상해질 수 있다.
넷째, 정렬과 검색의 순서를 헷갈리는 경우다. 보통은 먼저 검색으로 범위를 줄이고, 그 결과를 다시 정렬하는 편이 이해하기 쉽다. 물론 반대로 해도 되는 경우가 있지만, 입문 단계에서는 검색 후 정렬로 생각하는 편이 훨씬 덜 헷갈린다.
AI에게 이렇게 요청하면 좋다
이번 장도 Claude Code나 Codex와 함께 작업하기 아주 좋다. 목표가 분명하고, 결과를 브라우저에서 바로 확인할 수 있기 때문이다. 다만 "검색 기능 넣어 줘"처럼 짧게만 말하면 AI가 생각보다 과한 코드를 제안할 수 있다. 예를 들어 검색 대상이 불명확하거나, 정렬 기준이 너무 많거나, 파일 구조가 뒤섞일 수 있다.
그래서 요청은 이렇게 구체적으로 하는 편이 좋다.
Next.js App Router 기준으로 글 목록 페이지에 기본 검색과 정렬 기능을 추가해줘.
목업 데이터는 data/posts.ts를 그대로 사용하고,
검색 대상은 title, summary, author로 해줘.
정렬은 최신순, 오래된순, 제목순만 넣어줘.
PostCard 컴포넌트는 재사용하고,
검색 결과가 없으면 안내 문구를 보여줘.
가능하면 검색과 정렬 로직은 별도 클라이언트 컴포넌트로 분리해줘.이 요청의 좋은 점은 범위가 분명하다는 것이다. AI는 방향이 선명할수록 더 쓸 만한 결과를 내놓는다.
그리고 결과를 받은 뒤에는 꼭 직접 확인해야 한다.
검색어를 입력했을 때 목록이 바로 바뀌는가?
제목뿐 아니라 작성자나 요약으로도 검색되는가?
최신순과 오래된순이 실제로 올바르게 바뀌는가?
결과가 없을 때 빈 화면 대신 안내 문구가 보이는가?
AI는 빠른 초안을 주지만, 맞는지 판단하는 사람은 결국 우리다.
기능이 작아도 서비스의 인상은 크게 달라진다
검색과 정렬을 붙이고 나면 목록 페이지의 분위기가 꽤 달라진다. 전에는 콘텐츠를 그냥 나열해 둔 느낌이었다면, 이제는 사용자가 직접 탐색할 수 있는 도구가 생긴다.
이 변화는 서비스 인상에 매우 중요하다. 사용자는 화려한 기능보다도 "내가 원하는 걸 쉽게 찾을 수 있는가"에 더 민감하게 반응한다. 그래서 작은 검색 기능 하나만 있어도 서비스가 훨씬 똑똑하고 실용적으로 느껴진다.
개발을 처음 하는 사람에게도 이 장은 자신감을 준다. 복잡한 서버나 데이터베이스 없이도, 프론트엔드만으로 사용성을 크게 개선할 수 있다는 걸 눈으로 확인하게 되기 때문이다.
다음 장으로 가기 전, 꼭 확인할 것
이번 장을 마쳤다면 아래 내용을 스스로 점검해 보자.
검색창 입력값에 따라 목록을 걸러낼 수 있는가?
정렬 기준을 바꾸면 같은 데이터라도 다른 순서로 보여줄 수 있는가?
검색과 정렬 기능을 목록 화면 위쪽에 자연스럽게 배치할 수 있는가?
결과가 없을 때도 사용자가 당황하지 않도록 안내할 수 있는가?
이 네 가지가 손에 익으면, 단순한 목록을 '탐색 가능한 서비스 화면'으로 바꾸는 기본기를 갖춘 셈이다.
마무리
이번 장에서 우리는 아주 대단한 기능을 만든 것처럼 보이지 않을 수도 있다. 검색창 하나, 정렬 메뉴 하나뿐이기 때문이다. 하지만 실제 서비스에서는 이런 작은 차이가 큰 체감 차이를 만든다.
사용자는 원하는 글을 더 빨리 찾을 수 있고, 자신에게 맞는 방식으로 목록을 볼 수 있다. 그 순간 서비스는 단순히 데이터를 보여주는 화면이 아니라, 사용자의 행동에 반응하는 제품처럼 느껴진다.
다음 장에서는 여기서 한 걸음 더 나아가, 보기 좋고 쓰기 쉬운 UI로 화면을 다듬어 볼 것이다. 같은 기능이라도 어떻게 보이고 어떻게 눌리는지에 따라 서비스의 완성도는 크게 달라진다. 이제 우리의 앱은 점점 더 '초보자가 만든 연습 화면'에서 '누군가 써도 괜찮은 서비스'로 변해 가고 있다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.