logo

DowanKim

2. 자동완성, 어떻게 구현할까

2025년 12월 6일

정적 인터랙티브 대시보드

Next.js로 범죄 데이터 대시보드 구축하기 - 기본 구조 및 검색 기능 구현

이제 기본적인 구조를 세팅하고자 합니다.

1. 기본 라우팅 및 레이아웃 설정

1.1 페이지 구조 생성

Next.js App Router 기준으로 4개 페이지를 구성했습니다:

app/
├── page.tsx                    # Home (/)
├── about/page.tsx              # About (/about)
├── dashboard/page.tsx          # Dashboard (/dashboard)
└── detail/[category_middle]/page.tsx  # Detail (동적 라우트)

각 페이지는 page.tsx로 구성되며, 폴더 구조가 URL 경로가 됩니다.

1.2 공통 레이아웃 구현

app/layout.tsx에서 루트 레이아웃을 구성했습니다:

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body> <div className="flex min-h-screen flex-col"> <Header /> <main className="flex-1">{children}</main> <Footer /> </div> </body> </html> ); }
  • flex min-h-screen flex-col: 전체 화면 높이, 세로 배치
  • flex-1: 메인 콘텐츠가 남은 공간을 차지
  • Header와 Footer는 모든 페이지에서 공통으로 표시

1.3 각 페이지 기본 구조

Home 페이지 (/)

  • Hero 섹션: "언제, 어디서 범죄가 가장 많이 발생할까?"
  • Key Metric: 총 데이터 건수 표시
  • 대시보드로 이동 버튼

About 페이지 (/about)

  • 프로젝트 소개
  • 기술 스택 표시
  • 데이터 출처

Dashboard 페이지 (/dashboard)

  • 검색 기능 (자동완성 포함)
  • 차트 영역 (요일별, 시간대별, 대분류 비중)
  • 범죄 목록 리스트

Detail 페이지 (/detail/[category_middle])

  • 동적 라우트로 범죄 유형별 상세 정보 표시
  • URL 파라미터로 범죄 유형을 받아 처리

2. 컴포넌트 분리 (Header, Footer)

2.1 컴포넌트 분리의 이유

재사용성과 유지보수를 위해 별도 컴포넌트로 분리했습니다.

2.2 Header 컴포넌트

components/Header.tsx:

import Link from "next/link"; export default function Header() { return ( <header className="border-b border-gray-200 bg-white"> <nav className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4"> <Link href="/" className="text-xl font-bold text-gray-900"> Crime Insight 2019 </Link> <div className="flex gap-6"> <Link href="/"></Link> <Link href="/about">소개</Link> <Link href="/dashboard">대시보드</Link> </div> </nav> </header> ); }
  • Next.js의 Link 컴포넌트 사용 (클라이언트 사이드 네비게이션)
  • Tailwind CSS로 스타일링
  • 반응형 레이아웃 (sm:px-6 lg:px-8)

2.3 Footer 컴포넌트

components/Footer.tsx:

export default function Footer() { return ( <footer className="border-t border-gray-200 bg-white"> <div className="mx-auto max-w-7xl px-4 py-6"> <p className="text-center text-sm text-gray-500"> © 2024 Crime Insight 2019. 공공데이터포털 경찰청 데이터 기반. </p> </div> </footer> ); }

2.4 레이아웃에서 컴포넌트 사용

app/layout.tsx에서 import하여 사용:

import Header from "@/components/Header"; import Footer from "@/components/Footer";
  • @/tsconfig.json의 path alias로 프로젝트 루트를 가리킵니다.

3. 동적 라우트 구현 (Detail 페이지)

3.1 동적 라우트의 필요성

여러 범죄 유형을 하나의 컴포넌트로 처리하기 위해 동적 라우트를 사용해야 합니다.

3.2 동적 라우트 구조

Next.js App Router에서 동적 라우트는 폴더명을 대괄호로 감싸면 됩니다:

app/detail/[category_middle]/page.tsx
  • [category_middle]: URL 파라미터 이름
  • /detail/사기, /detail/절도 등으로 접근 가능

3.3 파라미터 받기

interface DetailPageProps { params: { category_middle: string; }; } export default function DetailPage({ params }: DetailPageProps) { const categoryName = decodeURIComponent(params.category_middle); return ( <h1>{categoryName} 범죄 분석 보고서</h1> ); }
  • params: URL 파라미터 객체
  • decodeURIComponent: URL 인코딩된 문자열 디코딩 (한글 처리)

3.4 페이지 재활용

하나의 컴포넌트로 여러 범죄 유형을 처리합니다:

  • /detail/사기 → "사기 범죄 분석 보고서"
  • /detail/절도 → "절도 범죄 분석 보고서"
  • /detail/강도 → "강도 범죄 분석 보고서"

각 범죄마다 별도 페이지를 만들지 않고, URL 파라미터로 동적 처리합니다.

4. Dashboard 검색 기능 구현

4.1 클라이언트 컴포넌트로 전환

검색은 실시간 필터링이 필요하므로 클라이언트 컴포넌트로 구현했습니다:

"use client"; import { useState } from "react"; export default function Dashboard() { const [searchQuery, setSearchQuery] = useState(""); // ... }
  • "use client": 클라이언트 컴포넌트 지시자
  • useState: 검색어 상태 관리

4.2 실시간 필터링 로직

const mockCrimeList = ["사기", "절도", "강도", "살인", ...]; const filteredCrimeList = mockCrimeList.filter((crime) => crime.includes(searchQuery) );
  • filter: 검색어가 포함된 항목만 필터링
  • 한글 검색이므로 대소문자 변환 없이 그냥 includes 사용

4.3 검색 폼 구현

<form onSubmit={handleSearch} className="flex gap-2"> <input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="..." /> <button type="submit">검색</button> </form>
  • onChange: 입력 시 실시간 필터링
  • onSubmit: 엔터 키 또는 버튼 클릭 시 처리

4.4 검색 결과 표시

{searchQuery ? ( filteredCrimeList.length > 0 ? ( <ul> {filteredCrimeList.map((crime) => ( <li> <Link href={`/detail/${encodeURIComponent(crime)}`}> {crime} </Link> </li> ))} </ul> ) : ( <div>검색 결과가 없습니다.</div> ) ) : ( <ul>{/* 전체 목록 */}</ul> )}
  • 조건부 렌더링으로 검색 상태에 따라 다른 UI 표시
  • encodeURIComponent: URL에 안전하게 한글 포함

5. 자동완성 기능 구현

5.1 자동완성의 필요성

검색창이 있으나 범죄의 종류는 제한되어 있으므로, 자동완성으로 사용자 입력을 제한 시킬 필요가 있어 입력창 바로 아래에 자동완성 드롭다운을 추가했습니다.

5.2 상태 관리

const [showAutocomplete, setShowAutocomplete] = useState(false); const searchContainerRef = useRef<HTMLDivElement>(null);
  • showAutocomplete: 드롭다운 표시 여부
  • searchContainerRef: 외부 클릭 감지를 위한 ref

5.3 자동완성 드롭다운 UI

<div ref={searchContainerRef} className="relative"> <input ... /> {showAutocomplete && searchQuery && filteredCrimeList.length > 0 && ( <div className="absolute z-10 w-full mt-1 bg-white border rounded-lg shadow-lg"> <ul> {filteredCrimeList.map((crime) => ( <li> <Link href={`/detail/${encodeURIComponent(crime)}`}> {crime} </Link> </li> ))} </ul> </div> )} </div>
  • relative: 부모 요소를 기준으로 위치 지정
  • absolute: 입력창 바로 아래에 배치
  • z-10: 다른 요소 위에 표시
  • 조건부 렌더링: 검색어가 있고 결과가 있을 때만 표시

5.4 외부 클릭 감지

useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node) ) { setShowAutocomplete(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []);
  • useEffect: 컴포넌트 마운트 시 이벤트 리스너 등록
  • contains: 클릭이 컨테이너 외부인지 확인
  • cleanup 함수로 이벤트 리스너 제거

5.5 입력 이벤트 처리

onChange={(e) => { setSearchQuery(e.target.value); setShowAutocomplete(true); }} onFocus={() => { if (searchQuery) { setShowAutocomplete(true); } }}
  • onChange: 입력 시 자동완성 표시
  • onFocus: 포커스 시 검색어가 있으면 자동완성 표시

5.6 항목 클릭 처리

<Link href={`/detail/${encodeURIComponent(crime)}`} onClick={() => { setShowAutocomplete(false); setSearchQuery(crime); }} > {crime} </Link>
  • 클릭 시 자동완성 닫기
  • 검색어를 선택한 항목으로 설정

6. 프로젝트 구조 최종 정리

staticpageproject/
├── app/
│   ├── layout.tsx              # 루트 레이아웃
│   ├── page.tsx                # Home
│   ├── about/page.tsx          # About
│   ├── dashboard/page.tsx      # Dashboard (검색 기능)
│   └── detail/
│       └── [category_middle]/
│           └── page.tsx         # 동적 Detail 페이지
├── components/
│   ├── Header.tsx              # 공통 헤더
│   └── Footer.tsx              # 공통 푸터
└── data/
    └── crime_data.csv          # CSV 데이터 파일

다음 단계

현재까지 구현된 내용:

  • ✅ 기본 라우팅 구조
  • ✅ 공통 레이아웃 및 컴포넌트 분리
  • ✅ 동적 라우트 구현
  • ✅ 검색 기능 및 자동완성

추가 구현 예정:

  • CSV 데이터 파싱 및 연동
  • 차트 시각화 (Recharts)
  • 데이터 기반 통계 분석