Hi, I’m Kang Byeong-hyeon.

..

[우선] 디자이너 피드백을 반영한 2차 MVP 제작기




1차 MVP에 대한 디자이너 피드백

팀원 중 한 분의 지인 중에 UI/UX 디자이너가 있다는 사실을 알게 되었다.

마침 1차 MVP를 배포한 후 디자인 완성도를 끌어올리고 싶은 욕심이 있었는데, 다행히도 디자이너분께서 피드백을 주시기로 하셔서 너무나도 감사했다. 🙇‍♂️

디자이너 분께서 기존 MVP 디자인을 바탕으로 리디자인을 해주셨다.

리디자인된 모바일 디자인 시안은 다음과 같다.



모바일 형태의 디자인 시안을 받았기 때문에, 데스크탑 디자인은 스스로 구상해야했다.

때문에 다른 레퍼런스 사이트를 참고하여 모바일, 태블릿, 데스크탑에서는 각 세션의 컨텐츠 들을 어떻게 배치하는지를 파악해보고 이를 바탕으로 우리 서비스에서는 어떻게 구성해야할지 고민하게 되었다.




변동사항 구현하기

뷰포트 노출 감지 구현

useIntersectionObserver hook 구현

import React, { useEffect, useRef, useState } from "react";

const useIntersectionObserver = (targetRef: React.RefObject<Element>) => {
  const [isInViewport, setIsInViewport] = useState(false);
  const observer = useRef<IntersectionObserver | null>(null);

  useEffect(() => {
    if (!observer.current) {
      observer.current = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            setIsInViewport(entry.isIntersecting);
          });
        },
        {
          threshold: 0,
        }
      );
    }

    if (targetRef.current) {
      observer.current.observe(targetRef.current);
    }

    return () => {
      observer.current?.disconnect();
    };
  }, [targetRef]);

  return isInViewport;
};

export default useIntersectionObserver;

사용자가 뷰포트에 노출되었을 때 description이 위로 올라가는 애니메이션을 적용하기 위해 Intersection Observer API를 활용하여 뷰포트와 타겟 요소의 중첩을 감지하는 Hook을 구현했다.

해당 Hook은 isInViewport 상태를 정의하여 타겟 요소가 뷰포트에 있는지 여부를 확인하도록 하였다.

그리고 useEffect를 통해 컴포넌트가 마운트될 때와 targetRef가 변경될 때 IntersectionObserver를 초기화하고, 타겟 요소가 뷰포트에 들어올 때 isInViewport 상태를 업데이트하도록 하였다.

최종적으로 해당 isInViewport 상태를 반환하도록 구현하였다.

InviewAnimation 컴포넌트 구현

import { ReactNode, useRef } from "react";
import * as S from "./InViewAnimation.styled";
import useIntersectionObserver from "@hooks/useIntersectionObserver";

interface InViewAnimationProps {
  children: ReactNode;
}

const InViewAnimation = ({ children, ...props }: InViewAnimationProps) => {
  const ref = useRef<HTMLDivElement>(null);
  const isInViewport = useIntersectionObserver(ref);

  return (
    <S.Layout
      ref={ref}
      className={`${isInViewport ? "animation" : ""}`}
      {...props}
    >
      {children}
    </S.Layout>
  );
};

export default InViewAnimation;
import styled from 'styled-components'

export const Layout = styled.div`
  will-change: transform, opacity;

  &.animation {
    animation-name: up;
    animation-duration: 1500ms;
    animation-timing-function: ease-out;

    @keyframes up {
      from {
        opacity: 0;
        transform: translateY(7%);
      }
      to {
        opacity: 1;
        transform: translateY(0%);
      }
    }
  }

이전에 구현한 useIntersectionObserver Hook을 활용하여 감지 여부에 따른 애니메이션 스타일을 정의해보았다.

그리고 InViewAnimation 컴포넌트를 만들어 해당 컴포넌트에 래핑된 부분이 뷰포트에 감지되었을 때 해당 애니메이션이 작동하도록 구현해보았다.

결과물

Aug-05-2024 14-34-14.gif

기업리스트 Marquee 구현

import Marquee from "react-fast-marquee";
import marqueeImages from "@/assets/images/marquee";
import * as S from "./CompanyMarquee.styled";
import { InViewAnimation } from "@components/common";

const CompanyMarquee = () => {
  return (
    <S.Layout>
      <InViewAnimation>
        <S.Title>
          많은 기업들의 멘토분들이
          <br />
          기다리고 있어요.
        </S.Title>
      </InViewAnimation>
      <Marquee>
        {marqueeImages.map((image, index) => (
          <S.ImageContainer key={index}>
            <S.Image src={image} />
          </S.ImageContainer>
        ))}
      </Marquee>
    </S.Layout>
  );
};

export default CompanyMarquee;

기업 리스트를 무한으로 왼쪽에서 오른쪽으로 움직이게 하기 위해 react-fast-marquee 라이브러리를 활용했다.

react-fast-marquee 라이브러리에서는 Marquee 컴포넌트 제공하는데, 해당 컴포넌트를 사용하여 이미지 리스트를 감싸 애니메이션이 동작될 수 있도록 구현했다.

결과물

Aug-05-2024 14-56-52.gif

멘토 프로필 슬라이더 구현

멘토 프로필들을 자동으로 왼쪽에서 오른쪽으로 슬라이드 될 수 있도록 구현해야했다.

자동으로 슬라이드될 수 있도록 react-slick 라이브러리를 활용하게 되었다.

react-slick에서는 Slider라는 컴포넌트를 제공하고 있었고, 해당 Slider 컴포넌트를 사용하여 sliderSettings를 통해 설정값을 전달하여 슬라이드를 구성하게 되었다.

export const getSliderSettings = (isDesktopOrLaptop: boolean) => ({
  className: "center",
  centerPadding: "20px",
  dots: false,
  centerMode: true,
  infinite: true,
  speed: 700,
  swipe: false,
  arrows: false,
  autoplay: true,
  slidesToShow: isDesktopOrLaptop ? 2 : 1,
  autoplaySpeed: 3000,
});

sliderSettings 값은 getSliderSettings 통해 슬라이더 설정을 반환하도록 하는 함수를 구현하였다.

isDesktopOrLaptop 인자를 받아 화면 크기에 따라 슬라이더 설정을 반환하도록 구현하였다.

해당 설정에서는 slidesToShow 속성을 통해 데스크탑에서는 2개의 슬라이드를, 그렇지 않은 경우에는 1개의 슬라이드를 보여주도록 하였다.

그 외에도 슬라이더의 속도(speed), 자동 재생(autoplay), 재생 속도(autoplaySpeed), 무한 반복(infinite) 등 다양한 설정들을 설정하여 반환하도록 구현하였다.

import { useMediaQuery } from "react-responsive";
import { getSliderSettings } from "@/utils/getSliderSettings";
import Slider from "react-slick";
import { mentorList } from "@/utils/constants/mentorList";
import MentorProfile from "../MentorProfile/MentorProfile";
import * as S from "./MentorProfileSlider.styled";

const MentorProfileSlider = () => {
  const isDesktopOrLaptop = useMediaQuery({ query: "(min-width: 1024px)" });
  const sliderSettings = getSliderSettings(isDesktopOrLaptop);

  return (
    <S.SliderContainer>
      <Slider {...sliderSettings}>
        {mentorList.map(({ id, image, company, job, specList }) => (
          <div key={id}>
            <MentorProfile
              imge={image}
              company={company}
              job={job}
              specList={specList}
            />
          </div>
        ))}
      </Slider>
    </S.SliderContainer>
  );
};
export default MentorProfileSlider;

화면 반응형 여부를 확인하기 위해 react-responsiveuseMediaQuery를 사용했다.

query property에 1024px 이상인 경우, 데스크탑에 해당한다고 가정하고 true를 반환하고, 그렇지 않은 경우 false를 반환하도록 하여 isDesktopOrLaptop 변수에 할당하였다.

이후 Slider 컴포넌트에 props로 해당 설정값을 전달하여 멘토 프로필들을 자동으로 슬라이드될 수 있도록 구현하였다.

결과물

Aug-05-2024 14-58-50.gif




2차 최종 MVP 결과물

screencapture-wooseon-2024-06-21-14_59_54.png




마치면서

MVP를 리디자인 하는 작업에서 많은 것을 배울 수 있었던 것 같다.

특히, Intersection Observer API와 react-slick, react-fast-marquee 라이브러리를 활용하여 다양한 애니메이션과 슬라이더를 구현하는 경험은 처음이기도 하고 많이 사용되는 애니메이션이기 때문에 해당 기술에 대해서 잘 숙지하고 있어야겠다고 생각했다.

특히 디자이너의 피드백을 받아 사이트의 디자인 완성도를 한층 더 끌어올릴 수 있어서 기분이 매우 좋았다!

디자인적인 개선뿐만 아니라, 사용자 경험(UX) 측면에서도 큰 향상을 이룰 수 있을 것 같은 기대도 들었다.

이제 이렇게 수정된 2차 MVP 페이지를 다른 팀원들에게 공유하고, 추가 피드백을 받을 계획이다.

팀원들의 피드백을 반영하여 최종적으로 배포를 진행할지 여부에 대해 의논해볼 예정이다.