시니어 케어 정보 플랫폼 시니어 숲을 개발하면서 핵심 기능 중 하나인 시설 비교(Comparison) 화면을 대대적으로 개선했습니다. 단순한 UI 리터치를 넘어, 데이터 설계부터 URL 보안, 소셜 공유 최적화까지 다양한 측면을 다루게 되어 그 과정을 공유합니다.
1. 배경: 무엇이 문제였나
초기 비교 기능은 두 시설의 기본 정보만 카드 형태로 나열하는 수준이었습니다. 사용자 입장에서 "이 시설이 저 시설보다 나은가?"를 판단하려면 각 시설 상세 페이지를 번갈아 켜야 하는 번거로움이 있었습니다. 구체적인 문제점은 다음과 같았습니다.
- 비교 지표 부족: 정원, 등급 수준의 정보만 노출
- 인력 정보 부재: 요양보호사·간호사·사회복지사 등 세부 인원 없음
- URL 보안: 내부 DB의 raw numeric ID가 URL에 그대로 노출
- 공유 경험: 카카오톡으로 공유 시 이미지가 없거나 비율이 맞지 않음
- 레이아웃 버그: 하단 CTA 버튼이 콘텐츠를 가리는 현상
2. 비교 UI 전면 개편
카테고리 기반 지표 구조
기존의 단순 나열 방식에서 벗어나, 지표를 4개 카테고리로 분류했습니다.
| 카테고리 | 주요 지표 |
|---|---|
| 기본 정보 | 시설 유형, 평가 등급, 평가 총점, 거리 |
| 입소 현황 | 정원, 현원, 잔여, 대기자, 충원율 |
| 비용 | 식비(일) |
| 인력 및 서비스 | 인력 비율, 요양보호사, 간호사, 사회복지사, 물리·작업치료사, 프로그램 수 |
각 카테고리는 METRIC_CATEGORIES 배열로 정의하여 추후 지표 추가 시 배열에만 항목을 추가하면 되도록 확장성을 확보했습니다.
export const METRIC_CATEGORIES = [
{
id: "occupancy",
label: "입소 현황",
icon: Users,
metrics: [
{ label: "정원", key: "capacity", suffix: "명" },
{ label: "잔여", key: "available", suffix: "명", best: "max" },
{ label: "대기자", key: "waitingCount", suffix: "명", best: "min" },
// ...
]
},
// ...
];
"우수 항목" 자동 하이라이팅
best: "max" 또는 best: "min" 속성을 통해 두 시설 중 더 나은 값을 자동으로 강조 표시합니다. 동점이거나 비교 불가능한 경우(문자열 타입 등)는 하이라이팅을 생략합니다.
function getBestSide(key, a, b, type) {
const va = getVal(a, key);
const vb = getVal(b, key);
if (typeof va !== 'number' || typeof vb !== 'number') return null;
if (va === vb) return null;
return type === 'max' ? (va > vb ? 'a' : 'b') : (va < vb ? 'a' : 'b');
}
종합 비교 요약 카드
모든 지표 비교가 끝난 최하단에 "종합 비교" 카드를 배치했습니다. 각 시설의 우수 항목 수를 집계해 시각적인 진행 바와 함께 표시합니다.
3. 동적 OG 메타데이터
Next.js의 [generateMetadata] 함수를 활용하여 비교 대상 시설에 맞는 맞춤형 메타데이터를 생성합니다.
export async function generateMetadata({ searchParams }) {
const ids = decodeIds(await searchParams.ids);
const facilities = await getFacilitiesByIds(ids);
const title = `${facilities[0].name} VS ${facilities[1].name} | 시설 상세 비교`;
// ...
return {
title,
openGraph: { title, images: [{ url: `/api/og?title=...` }] },
twitter: { card: "summary_large_image" },
};
}
이렇게 하면 카카오톡이나 트위터로 링크를 공유할 때 "성남시립노인요양센터 VS 자광원 | 시설 상세 비교"와 같이 의미 있는 제목이 미리보기로 표시됩니다.
4. 카카오톡 공유 이미지 최적화 (1:1 비율)
카카오톡 피드에서 링크 미리보기 이미지가 잘리는 문제가 있었습니다. /api/og 라우트에 square=true 파라미터를 추가하여 800×800 정사각형 이미지를 생성하도록 했고, SDK 호출 시에도 동일한 크기를 명시합니다.
const ogImageUrl = new URL(`${window.location.origin}/api/og`);
ogImageUrl.searchParams.set('title', shareTitle);
ogImageUrl.searchParams.set('square', 'true');
window.Kakao.Share.sendDefault({
objectType: 'feed',
content: {
imageUrl: ogImageUrl.toString(),
imageWidth: 800,
imageHeight: 800,
// ...
},
});
카카오톡은 imageWidth·imageHeight를 기반으로 이미지 렌더링 비율을 결정하므로, 두 값을 동일하게 설정하면 피드에서 정사각형으로 깔끔하게 표시됩니다.
5. 레이아웃 버그: 플로팅 버튼 vs. 콘텐츠 겹침
문제
비교 페이지 최하단에 position: fixed로 고정된 "더 많은 시설 찾아보기" 버튼이 스크롤 끝에서 "종합 비교" 카드를 가리는 현상이 발생했습니다.
┌─────────────────────┐
│ 종합 비교 카드 │
│ ╔══════════════╗ │ ← 버튼이 카드 위를 덮음
│ ║ 더 많은 시설 ║ │
│ ╚══════════════╝ │
└─────────────────────┘
원인 분석
[ResponsiveShell]의 main 요소가 overflow-y-auto로 스크롤을 담당하고 있었고, 내부 콘텐츠의 padding-bottom 값이 버튼 높이보다 작아 최하단 콘텐츠가 버튼 아래로 스크롤되지 않는 구조였습니다.
Tailwind 유틸리티 클래스(pb-48, h-80)를 추가해도 computed height가 0px로 렌더링되는 현상을 확인 — CSS 캐시나 purge 설정 문제로 추정됩니다.
해결: 인라인 스타일 스페이서
Tailwind 대신 인라인 스타일로 스페이서를 직접 지정하여 확실하게 공간을 확보했습니다.
// ComparisonView.tsx
<WinnerSummary facA={facA} facB={facB} />
{/* 플로팅 버튼이 콘텐츠를 가리지 않도록 하단 여백 확보 */}
<div style={{ height: '80px' }} aria-hidden="true" />
// compare/page.tsx
<div className="bg-background" style={{ paddingBottom: '128px' }}>
인라인 스타일은 Tailwind의 빌드 과정에 의존하지 않아 어떤 환경에서도 안정적으로 동작합니다.
6. 정리 및 회고
이번 개선을 통해 얻은 교훈을 정리하면 다음과 같습니다.
✅ 잘 된 점
- 카테고리 기반 설계로 향후 지표 추가가 매우 용이해졌습니다.
- [generateMetadata]를 서버 컴포넌트 수준에서 처리하여 별도 API 없이 SEO를 개선했습니다.
⚠️ 개선할 점
- Tailwind 클래스가 런타임에 동적으로 생성될 경우 purge에서 제거될 수 있으니, 임의값 클래스는 safelist에 미리 등록하거나 인라인 스타일을 활용하는 것이 안전합니다.
- 카카오톡 SDK의 이미지 크기 명세는 공식 문서에 명확히 나와 있지 않아 실제 테스트로 확인할 필요가 있었습니다.
관련 기술 스택
- Frontend: Next.js 15 (App Router), React, TypeScript
- UI: Tailwind CSS, Framer Motion, Lucide React
- 공유: Kakao SDK (
Kakao.Share.sendDefault) - 이미지 생성: Next.js
ImageResponse - DB ORM: Drizzle ORM
'사이드프로젝트' 카테고리의 다른 글
| Next.js로 만든 장기요양등급 자가진단 모의평가 — 설계부터 SEO까지 (0) | 2026.02.27 |
|---|---|
| [지도 기반 서비스] 검색 UX와 렌더링 성능 최적화 경험기 (0) | 2026.02.25 |
| [개선] 모바일 뷰포트 수정과 직관적인 지도 검색 (0) | 2026.02.18 |
| 커뮤니티 기능 개발 전 네이버 블로그 RSS & API로 연동하기 (0) | 2026.02.12 |
| Antigravity와 험난한 UI 수정기 (0) | 2026.02.11 |