개요 페이지의 UX 사례 연구

모든 포스트 카테고리에 대한 개요 페이지를 디자인한 방법

UI 및 UX 목표

게시된 게시물의 모든 카테고리를 표시하는 개요 페이지의 주요 목표는 실제로 사용 가능한 카테고리의 검색 가능성을 높이는 것이었습니다. 나는 또한 이 변화가 페이지의 시각적 아름다움을 향상시킬 뿐만 아니라 이 프로그레시브 웹 앱의 다른 페이지와 UX를 정렬할 수 있는 기회로 보았습니다.

다음 스크린샷은 페이지가 재설계되기 전의 모습을 보여줍니다.

Image e8edca7d7ea0

바꿔야 했던 것

이전 구현에는 두 가지 주요 문제가 있었습니다. 첫째, 요소가 5개 미만인 카테고리는 전체 너비에 맞게 사용 가능한 미리보기 후 표지 이미지의 크기를 조정했기 때문에 레이아웃이 실제로 유효하지 않았습니다. 둘째, 페이지 상단에 기본 작업 표시줄이 표시되지 않았습니다. 사용자에게 기본 탐색 요소를 제공하므로 반드시 렌더링되어야 합니다.

또한 카테고리에 대한 개요 페이지가 실제로 카테고리를 표시하는 데 큰 역할을 하지 않았다는 것이 분명해졌습니다. 대신 사용자는 주로 각 카테고리와 관련된 게시물의 표지 이미지를 보았습니다. 다시 살펴봐야만 아이콘이 있는 행과 각 카테고리의 제목이 인식됩니다. 이 UI는 미학적으로 좋지 않은 것 외에도 단순히 잘못 설계되었습니다.

실수 수정

이것은 내 개인 웹 앱이기 때문에 카테고리 개요 페이지에서 처음에 일을 엉망으로 만든 사람이기도 합니다. 더 이상 변명의 여지 없이 나는 앉아서 전체 페이지를 다시 작업했습니다.

새 버전은 행의 단일 열을 표시하며, 각 항목은 범주에 대한 큰 미리보기 이미지를 표시합니다. 사용자 정의 이미지는 허용된 전체 너비를 사용합니다. 두 축의 중앙에는 중간에 카테고리 제목이 있어 즉시 눈에 띄고 알아볼 수 있습니다. 가독성을 높이기 위해 텍스트 아래에 검은색에서 투명하게 미묘한 그라디언트를 추가했습니다.

이미지와 텍스트 위로 마우스를 가져가면 추가 정보 텍스트가 표시되어 이전에는 볼 수 없었던 이 카테고리의 총 게시물 수를 보여줍니다.

탐색 문제를 해결하기 위해 사용자가 사용할 수 있는 모든 주요 경로와 함께 기존 작업 표시줄도 재사용했습니다.

Image 0616bdd11b98

반복을 통한 개선

보다 쾌적한 사용자 경험을 제공하기 위해 카테고리 행 위로 마우스를 가져갈 때 이미지에 미묘한 크기 조정 애니메이션을 추가했습니다. 이미지가 원래 경계 내에서 확장됨에 따라 보기가 불필요하게 커지지 않습니다.

또한 카테고리 이미지 아래에 있는 새 행은 그 위에 마우스를 올리면 애니메이션을 통해 표시됩니다. 이 행은 카테고리에 대한 최신 게시물의 선택을 보여줍니다. 각 게시물 미리보기는 게시물에 대한 링크입니다. category-row의 다른 모든 대화형 요소는 해당 범주에 대한 세부 정보 페이지를 엽니다.

실제보다 더 많은 콘텐츠가 있다는 환상을 주기 위해 저는 Daisy-UI의 "스택" 클래스를 사용하여 각 사후 미리보기에 가짜 스택을 제공하여 현재 항목 아래에 더 많은 미리보기 항목이 있다는 인상을 암시했습니다.

Image 41c052a8c928

작은 장치에서는 미리보기가 렌더링되지 않습니다. 사용자는 큰 카테고리 이미지를 클릭하기만 하면 모든 게시물이 나열되는 세부 정보 페이지로 이동할 수 있습니다.

검토 및 전망

보시다시피, 요소에 대한 개요만 제공하면 되는 단순한 페이지라도 UX는 물론 훌륭하고 작동하는 UI를 디자인하는 방법에 대한 학습 자료일 뿐만 아니라 시각적 및 개념적 오류의 원인이 될 수 있습니다.

사용자가 텍스트 입력으로 필터링할 수 있도록 로컬 텍스트 검색을 추가하지 않았습니다. 지금은 스크롤과 호버링을 통해 페이지 탐색을 촉진하려고 합니다. 이것이 나중에 문제가 된다면(예: 사용자가 원하는 콘텐츠를 찾을 수 없기 때문에) 필터링 옵션을 추가하겠습니다. 게시물의 수와 카테고리가 계속 증가함에 따라 이는 필요할 수도 있습니다. 물론, 나는 당신이 직접 페이지를 사용해 보는 것을 환영합니다.

소스 코드

다음 텍스트에는 페이지 컨테이너와 범주 그룹 모두에 대한 수정되지 않은 완전한 코드가 포함되어 있습니다. 모든 것은 React.js 및 Tailwind.css를 사용하여 Typescript로 작성되었습니다. 일부 부품을 재사용할 수 있다고 생각하시면 기꺼이 도와드리겠습니다.

//
// Page container.
//

import React from "react";
import { CMSCategoryResolution } from "../../cms/entities/cms.entity.catgory";
import Reveal from "../Reveal/Reveal";
import { BogPostCategoryPostsGroup } from "./BlogPostCategoryPostsGroup";
import BlogPostCategoriesOverviewHero from "./BlogPostCategoriesOverviewHero";
import PrimaryActionBar from "../PrimaryActionsBar/PrimaryActionsBar";

/*
 *
 * Interfaces.
 *
 */

interface Props {
  categories: CMSCategoryResolution[];
}

/*
 *
 * Components.
 *
 */

export default function BlogPostCategoriesOverview(props: Props) {
  const { categories } = props;

  return (
    <div className="max-w-6xl mx-auto">
      <BlogPostCategoriesOverviewHero />
      <div className="mt-10 mb-20">
        <PrimaryActionBar isCentered color="white" />
      </div>
      <div className="px-6 mx-auto space-y-6 lg:px-0 md:space-y-16 lg:space-y-20">
        {categories.map((c, i) => (
          <Reveal key={i}>
            <BogPostCategoryPostsGroup category={c} maxPostsEachThreshold={20} />
          </Reveal>
        ))}
      </div>
    </div>
  );
}

//
// BogPostCategoryPostsGroup.
//

import React from "react";
import { CMSCategoryResolution } from "../../cms/entities/cms.entity.catgory";
import { FLPath } from "../../models/route/model.route";
import { CMSCategoryIconFactory } from "../CMS/CMSCategoryIcon";
import Link from "next/link";
import RiMore from "remixicon-react/MoreLineIcon";

/*
 *
 * Interfaces.
 *
 */

interface Props {
  category: CMSCategoryResolution;
  maxPostsEachThreshold: number;
}

/*
 *
 * Components.
 *
 */

export const BogPostCategoryPostsGroup = (props: Props) => {
  const { category, maxPostsEachThreshold } = props;
  const { posts } = category;

  if (posts.length === 0) {
    return null;
  }

  const url = `${FLPath.Categories}/${category.slug.current}`;
  const Icon = CMSCategoryIconFactory({ variant: category.slug.current });

  return (
    <div className="flex flex-col items-center group">
      <div className="relative max-w-4xl overflow-visible rounded-md">
        <div className="absolute items-end justify-start hidden w-full h-full px-6 space-x-4 overflow-x-scroll duration-500 ease-in-out translate-y-12 opacity-0 md:flex pb-14 group-hover:opacity-100 group-hover:translate-y-20">
          {posts.slice(0, 4).map((p, i) => (
            <span className="stack">
              <Link
                href={`${FLPath.Posts}/${p.slug.current}`}
                aria-label={p.title}
                prefetch={false}>
                <a className="duration-200 ease-out hover:scale-105">
                  <img
                    src={p.imageUrl.jpg}
                    alt={p.title}
                    loading="lazy"
                    className="object-contain h-20 rounded "
                  />
                </a>
              </Link>

              {Array.from({ length: 2 }, (_, i) => (
                <span key={i} style={{ width: 152, height: 80 }} className="bg-gray-800 rounded" />
              ))}
            </span>
          ))}
          {posts.length >= 5 && (
            <Link passHref href={url} aria-label={`Link to ${category.title}`} prefetch={false}>
              <a className="flex flex-col items-center justify-center h-20 w-14">
                <RiMore />
              </a>
            </Link>
          )}
        </div>

        <div className="relative flex flex-col items-center overflow-hidden duration-500 ease-in-out rounded-md shadow-xl md:group-hover:-translate-y-20">
          <img
            className="object-cover w-full duration-500 ease-in-out group-hover:scale-105"
            width={960}
            height={200}
            src={category.imageUrl.webp}
            alt={category.title}
            loading="lazy"
          />
          <Link passHref href={url} aria-label={`Link to ${category.title}`} prefetch={false}>
            <a className="absolute flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-primary">
              <span className="flex items-center space-x-2 duration-300 ease-in-out translate-y-2 group-hover:-translate-y-3">
                <Icon size="2em" />
                <h2 className="text-sm md:text-xl">{category.title}</h2>
              </span>
              <span className="text-sm font-light text-gray-200 transition duration-500 ease-in-out translate-y-2 opacity-0 group-hover:opacity-100 group-hover:-translate-y-2">
                {category.posts.length}
                {category.posts.length === maxPostsEachThreshold ? "+" : ""}{" "}
                {category.posts.length === 1 ? "post" : "posts"}
              </span>
            </a>
          </Link>
        </div>
      </div>
    </div>
  );
};