블로그 태그 필터 UI 개선: 모바일 터치부터 접근성까지

2026년 1월 19일
7 min read
6 views
React
UI/UX
Web Development
Accessibility
Mobile

시작: 작은 불편함의 발견

블로그에 태그 필터 기능을 추가하고 모바일에서 테스트하던 중 이런 생각이 들었습니다.

"태그 버튼이 너무 작아서 터치하기 어렵다..."

이 작은 불편함이 모바일 UX 개선의 시작이었습니다.

문제 1: 모바일 터치 영역 부족

증상

// 초기 구현
<Badge className="px-2 py-1">
  {tag}
</Badge>
  • 태그 버튼 높이: 약 28px
  • iOS/Android 권장 최소 터치 영역: 44px
  • 결과: 정확한 터치가 어려움

해결: 터치 영역 확보

<Badge
  className="min-h-[2.5rem] touch-manipulation px-3 py-2"
  onClick={() => handleTagClick(tag)}
>
  {tag}
</Badge>

적용한 최적화:

  • min-h-[2.5rem]: 최소 높이 40px 보장
  • touch-manipulation: 터치 지연 제거 (더블탭 줌 비활성화)
  • px-3 py-2: 충분한 내부 여백으로 터치 영역 확대

결과

터치 성공률이 눈에 띄게 향상되었고, 사용자가 의도한 태그를 정확히 선택할 수 있게 되었습니다.

문제 2: 태그 클릭 시 이벤트 전파

증상

PostCard 내부의 태그를 클릭하면 카드 전체가 클릭되어 상세 페이지로 이동

<Link href={`/blog/${post.slug}`}>
  <div className="post-card">
    {/* ... */}
    <Badge onClick={() => handleTagClick(tag)}>
      {tag}
    </Badge>
  </div>
</Link>

해결: 이벤트 전파 차단

const handleTagClick = (e: React.MouseEvent, tag: string) => {
  e.preventDefault();      // 기본 동작 방지
  e.stopPropagation();    // 부모 요소로 이벤트 전파 차단
  router.push(`/blog?tag=${encodeURIComponent(tag)}`);
};

적용:

<Badge
  onClick={(e) => handleTagClick(e, tag)}
  className="cursor-pointer hover:bg-primary/10"
>
  {tag}
</Badge>

문제 3: URL 파라미터 관리

왜 URL 파라미터인가?

태그 필터 상태를 관리하는 방법은 여러 가지가 있습니다:

  1. 컴포넌트 상태 (useState)
  2. 전역 상태 (Zustand, Redux)
  3. URL 파라미터

URL 파라미터를 선택한 이유:

  • 북마크 가능: /blog?tag=react를 저장하면 필터 상태 유지
  • 공유 가능: 링크를 공유하면 동일한 필터 적용
  • 새로고침 안전: 페이지를 새로고침해도 상태 유지

구현: 태그 토글 기능

const handleTagClick = (tag: string) => {
  const params = new URLSearchParams();
  
  if (selectedTag === tag) {
    // 같은 태그 재클릭 → 필터 해제
    params.delete("tag");
  } else {
    // 새 태그 선택
    params.set("tag", tag);
  }
  
  // 태그 변경 시 첫 페이지로
  params.delete("page");
  
  router.push(`/blog?${params.toString()}`);
  window.scrollTo({ top: 0, behavior: "smooth" });
};

URL 인코딩 처리

// 특수문자가 포함된 태그 안전하게 처리
router.push(`/blog?tag=${encodeURIComponent(tag)}`);

// 예시:
// "C++" → ?tag=C%2B%2B
// "Node.js" → ?tag=Node.js

문제 4: 태그 목록 UI 설계

요구사항

  • 모든 태그를 한눈에 볼 수 있어야 함
  • 현재 선택된 태그 시각적 표시
  • 모바일에서도 자연스러운 줄바꿈

구현

<div className="flex flex-wrap gap-2 -mx-1 px-1">
  {allTags.map((tag) => {
    const isSelected = selectedTag === tag;
    return (
      <Badge
        key={tag}
        variant={isSelected ? "default" : "gray"}
        className={`
          px-3 py-2 text-sm font-normal cursor-pointer 
          transition-all touch-manipulation 
          min-h-[2.5rem] flex items-center
          ${isSelected 
            ? "bg-primary text-primary-foreground hover:bg-primary/90" 
            : "hover:bg-primary/10 hover:text-primary"
          }
        `}
        onClick={() => handleTagClick(tag)}
      >
        {tag}
      </Badge>
    );
  })}
</div>

핵심 CSS:

  • flex-wrap: 태그가 많을 때 자동 줄바꿈
  • gap-2: 태그 사이 간격 균일하게
  • transition-all: 부드러운 상태 전환 애니메이션
  • 조건부 스타일: 선택 여부에 따라 다른 색상

필터 초기화 버튼

{selectedTag && (
  <Button
    variant="outline"
    size="sm"
    onClick={() => {
      router.push("/blog");
      window.scrollTo({ top: 0, behavior: "smooth" });
    }}
  >
    필터 초기화 ✕
  </Button>
)}

문제 5: 접근성 고려

ARIA 레이블 추가

<Badge
  role="button"
  aria-pressed={isSelected}
  aria-label={`${tag} 태그로 필터링`}
  onClick={() => handleTagClick(tag)}
>
  {tag}
</Badge>

키보드 네비게이션

<Badge
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      handleTagClick(tag);
    }
  }}
>
  {tag}
</Badge>

엣지 케이스 처리

1. 태그가 없는 포스트

// 옵셔널 체이닝으로 안전하게 처리
const matchTags = post.tags?.some(tag => 
  tag.toLowerCase().includes(searchLower)
) || false;

// UI에서도 안전하게
{post.tags?.map((tag) => (
  <Badge key={tag}>{tag}</Badge>
))}

2. 빈 검색 결과

{filteredPosts.length > 0 ? (
  filteredPosts.map((post) => <PostCard key={post.id} post={post} />)
) : (
  <motion.div 
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    className="text-center py-20 text-muted-foreground"
  >
    <p className="text-lg">검색 결과가 없습니다</p>
    <p className="text-sm mt-2">다른 검색어를 시도해보세요</p>
  </motion.div>
)}

최종 사용자 경험

Before

  • 태그 클릭이 부정확함
  • 카드 전체가 클릭됨
  • URL에 상태가 없어 공유 불가

After

  • 정확한 터치 입력
  • 태그만 독립적으로 클릭 가능
  • URL로 필터 상태 공유 가능
  • 모바일에서도 편안한 사용

결론

UI 개선은 작은 디테일의 합입니다.

핵심 교훈:

  1. 모바일 터치 영역은 최소 44px
  2. 이벤트 전파를 항상 고려하라
  3. 상태는 URL에 담아 공유 가능하게
  4. 접근성은 선택이 아닌 필수

작은 불편함을 발견하고 개선하는 과정이 좋은 사용자 경험을 만듭니다.