본문 바로가기
사이드프로젝트

[개선] 장기요양시설 비교 기능 개선기 — 데이터, UX, 공유까지

by geoyeon-ai 2026. 2. 20.

시니어 케어 정보 플랫폼 시니어 숲을 개발하면서 핵심 기능 중 하나인 시설 비교(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