02. GIFT 프로젝트 회고 : 페이지, 컴퍼넌트 소개

쏘쏘임·2022년 2월 2일
5

[project]GIFT

목록 보기
2/3
post-thumbnail

기간 : 22.01.06 ~ 22.01.28
이름 : GIFT
주제 : GIPHY 웹사이트 클론 및 개선
기술 : React, Typescript, styled-components, storybook, Redux, Webpack
링크 : GIFT 배포 링크
위키 : GIFT 깃허브 위키
팀원 : @congaweb, @jkpark104, @sosoYim

🚩 GIPHY 공식 사이트 giphy.com을 클론하며 성능과 접근성 및 UX를 개선하고자 하였고, 나아가 react, styled-components, typescript, storybook 등의 기술을 익히고자 하였습니다.


페이지 소개

메인 페이지

주요 컴퍼넌트 소개

프로젝트 진행 중 기억에 남는 컴퍼넌트들을 소개해보겠습니다.

✨ 로고 애니메이션

  1. 각각의 요소들이 순차적으로 그려짐
  2. 일정한 시간마다 애니메이션 반복 재생

이번 프로젝트에서 사용한 framer-motion은 요소들의 애니메이션을 선언적으로 통제하기에 유용한 라이브러리입니다. Framer motion 공식 문서 바로가기

svg 애니메이션은 opacity 또는 scaleX, scaleY 속성으로 구현하였습니다. 이를 순차적으로 재생하기 위해 라이브러리에서 제공하는 useAnimation 훅을 사용하였고, 실행시 애니메이션을 정의하고 실행시키는 sequenceAnimation 함수를 만들었습니다.

// useAnimation 훅을 사용하여 컨트롤 객체 생성
const control1 = useAnimation();
const control2 = useAnimation();

function sequenceAnimation() {
  // (기타 컨트롤 생략)
  // 초기 값 설정
  control1.set({ opacity: 0, scale: 0 });
  control2.set({ scaleX: 0, x: '-5%' });

  // promise를 반환하는 start 메서드를 호출하여 순차적으로 애니메이션을 실행하는 sequences 함수 만들기
  const sequences = async () => {
    await control1.start({
      opacity: 1,
      scale: 1,
      transition: { duration: 0.2 },
    });
    await control2.start({ scaleX: 1, x: 0 });
  };
  sequences();
}

useEffect 안에서 위에서 정의한 sequenceAnimation를 한 번 실행 시킵니다. 이 후 setInterval을 이용해 반복 실행 시켜주고, 한번 실행된 스케줄링함수는 clearId로 받아 제거해주었습니다.

  useEffect(() => {
    sequenceAnimation();
    const clearId = setInterval(sequenceAnimation, 6000);
    return () => clearInterval(clearId);
  }, [control1, control2, control3, control4, control5]);

✨ SvgIcon

  1. 장식적인 요소인지 의미를 가진 요소인지 구분
  2. svg-sprite 기법을 사용하여 svg 이미지 랜더링 - use 속성 사용
  3. 기본적으로 부모의 영역을 가득 채움
  4. size를 조절할 수도 있음 - width, height 속성 사용
  5. 단색일 경우 색을 변경할 수 있음 - fill 속성 사용

svg-sprite 기법을 사용하여 use 속성에 사용할 id를 props으로 받는 컴퍼넌트입니다. label props에 값을 전달하는지에 따라 장식적 요소로 사용할 수도, 의미를 가진 요소로 사용할 수도 있습니다.

장식적 요소로 사용 : 아래의 검색 버튼 내부에 사용된 아이콘은 버튼에 이미 search 라벨을 달고 있기 때문에 단순히 장식적인 요소로만 사용됩니다.

의미가 있는 요소로 사용 : 반면 아래와 같이 '검증되었다'는 의미를 가진 뱃지 요소는 label props에 verified를 전달하여 다음과 같이 타이틀을 가질 수 있게 하였습니다.

✨ A11yhidden

  1. 원하는 태그를 지정할 수 있어야 함
  2. 태그로 감싼 자식 요소들이 화면상에 보이지 않으나 보조기기에는 읽혀야 함
  3. focus 될 때 보여지도록 설정할 수 있어야 함

디자인상 보여지지 않는 헤딩 태그 및 라벨 등을 위해 사용하는 컴퍼넌트입니다. styled-components를 사용하기 때문에 as props로 어떤 html 태그를 사용할지 전달하면 됩니다. 컴퍼넌트 자식으로는 숨길 요소를 작성하면 됩니다.

  overflow: hidden;
  position: absolute;
  clip: rect(1px 1px 1px 1px);
  clip-path: circle(0);
  width: 1px;
  height: 1px;
  margin: -1px;
  white-space: nowrap;

A11yHidden 컴퍼넌트의 루트 요소에 위와 같은 스타일을 주어 화면상에 보이지 않도록 만들었습니다.
focus에 대한 처리는 $focasable props를 받아 이 값이 true인 경우에만 해당 요소가 :focus 일 때 위에서 숨긴 것의 반대 처리를 해주었습니다.

✨ 검색바

  1. 검색어를 입력할 때마다 이벤트 핸들러들이 재정의되지 않아야 함 - useCallback 사용
  2. 연관 검색어를 받아와 추천 - 디바운싱 기법 사용
  3. 같은 검색어를 submit할 경우 재랜더링 하지 않음 -submit 핸들러에서 제어

  • 문제 : 검색바(인풋)의 onBlur 이벤트에서, 초점이 나갈 경우 연관 검색어 컴퍼넌트를 감추었습니다. 이것 때문에 연관 검색어를 클릭한 경우, 링크 이동보다 먼저 컴퍼넌트가 감추어져 링크 이동이 불가능한 문제가 있었습니다.

  • 고민 : 가장 먼저 생각한 해결 방안은 연관 검색어 영역과 그 외의 영역을 구분하여 이벤트 처리를 하는 것이었습니다. 다만, 이러한 구분을 위해서는 가장 상단의 영역인 root 요소까지 이벤트 처리가 올라가야한다는 점이었습니다. 이는 컴퍼넌트 단위 개발 관점에서 적합하지 않아보였습니다.

  • 해결 : onBlur 내에서 처리하되, 연관 검색어에 대한 처리를 추가했습니다. 우선 연관검색어가 보여지는지 여부를 관리하는 상태 isOpen과 현재 페이지에서 검색된 키워드인 actualKeyword 상태를 두었습니다. 작업 과정은 다음과 같습니다.

    1. closest 를 이용해 포커스가 이동하는 요소인 relatedTarget의 랜더트리에 특정 클래스가 포함되는지 확인합니다.
    2. 검색바 혹은 연관검색어가 아니면 isOpen을 false로 바꿉니다.
    3. 연관 검색어 영역이라면 선택된 링크의 텍스트(연관 검색어)를 추출하여 actualKeyword 의 값을 업데이트합니다.
    4. 상태 변경으로 재랜더링이 발생하며 새로운 검색어 결과가 보여집니다.

✨ 헤더

  1. 반응형 - 윈도우 viewport 너비에 따라 조건부 렌더링
  2. 시맨틱한 구조 - 시맨틱 태그 사용
  3. 서브메뉴는 헤더의 상단 섹션 바로 하단에 위치 - position 속성 이용
  4. 재랜더링 최소화 - 헤더의 상단 부분 useMemo
  • 재랜더링 최소화 :

    • 문제 : 문자 입력, 화면 너비 조절시 재랜더링이 발생하는 검색바로 인해 헤더 상단부분도 함께 재랜더링
    • 해결 : useMemo를 이용해 검색바를 제외한 top-section 부분을 기억
  • 상태에 따른 조건부 렌더링으로 반응형 구현 :

    1. isMobile(boolean) 상태로 현재 모바일 버전인지 데스크탑 버전인지 관리
    2. document의 width 값과 기존의 isMobile 상태가 적합하지 않은 경우 상태 업데이트
      (width가 브레이크포인트 미만이면서 isMobile이 false일 때, 혹은 그 반대의 경우)

✨ Taglist


@congaweb님께서 제작해주셨고, 코드리뷰를 통해 작업 과정의 고민을 공유받을 수 있었습니다. 단순해 보이는 Taglist이지만 리액트의 동작 원리에 대한 이해가 필요한 작업이었습니다.

  • 미션 :
    • 현재 브라우저에서 출력된 gif의 가로 길이를 넘지 않는만큼 태그 리스트를 랜더링
    • 랜더링이 안된 태그가 남았다면 더보기(+) 버튼을 제공
    • gif의 크기는 고정되지 않은 반응형
    • tag에 들어갈 글자 수는 무제한
    • 글자마다 각기 다른 width를 가짐
  • 해결 : useLayout과 상태 변경을 통해 구현
    • 화면에 먼저 출력될 태그의 text들이 담긴 배열과 더보기 버튼을 눌렀을 때 나올 text들이 담긴 배열을 상태로 두었습니다.
    • 위의 조건에서 몇 개의 Tag 컴퍼넌트를 랜더링할지는 화면의 layout이 그려진 이 후에야 알 수 있었습니다. useLayout은 레이아웃이 측정된 상태라 필요한 태그들의 길이를 계산할 수 있고, 페인트 되기 이전 단계이기 때문에 재랜더링 시 깜빡임 없이 랜더링될 수 있었습니다.

마무리

다음 장에서는 성능 최적화 및 접근성 측면에서 GIFT 팀의 노력을 공유해보고, 총 마무리 회고를 적어보겠습니다!

profile
무럭무럭 자라는 주니어 프론트엔드 개발자입니다.

0개의 댓글