기간 : 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 등의 기술을 익히고자 하였습니다.
프로젝트 진행 중 기억에 남는 컴퍼넌트들을 소개해보겠습니다.
- 각각의 요소들이 순차적으로 그려짐
- 일정한 시간마다 애니메이션 반복 재생
이번 프로젝트에서 사용한 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]);
- 장식적인 요소인지 의미를 가진 요소인지 구분
- svg-sprite 기법을 사용하여 svg 이미지 랜더링 - use 속성 사용
- 기본적으로 부모의 영역을 가득 채움
- size를 조절할 수도 있음 - width, height 속성 사용
- 단색일 경우 색을 변경할 수 있음 - fill 속성 사용
svg-sprite 기법을 사용하여 use 속성에 사용할 id를 props으로 받는 컴퍼넌트입니다. label props에 값을 전달하는지에 따라 장식적 요소로 사용할 수도, 의미를 가진 요소로 사용할 수도 있습니다.
장식적 요소로 사용 : 아래의 검색 버튼 내부에 사용된 아이콘은 버튼에 이미 search 라벨을 달고 있기 때문에 단순히 장식적인 요소로만 사용됩니다.
의미가 있는 요소로 사용 : 반면 아래와 같이 '검증되었다'는 의미를 가진 뱃지 요소는 label props에 verified를 전달하여 다음과 같이 타이틀을 가질 수 있게 하였습니다.
- 원하는 태그를 지정할 수 있어야 함
- 태그로 감싼 자식 요소들이 화면상에 보이지 않으나 보조기기에는 읽혀야 함
- 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
일 때 위에서 숨긴 것의 반대 처리를 해주었습니다.
- 검색어를 입력할 때마다 이벤트 핸들러들이 재정의되지 않아야 함 - useCallback 사용
- 연관 검색어를 받아와 추천 - 디바운싱 기법 사용
- 같은 검색어를 submit할 경우 재랜더링 하지 않음 -submit 핸들러에서 제어
문제 : 검색바(인풋)의 onBlur 이벤트에서, 초점이 나갈 경우 연관 검색어 컴퍼넌트를 감추었습니다. 이것 때문에 연관 검색어를 클릭한 경우, 링크 이동보다 먼저 컴퍼넌트가 감추어져 링크 이동이 불가능한 문제가 있었습니다.
고민 : 가장 먼저 생각한 해결 방안은 연관 검색어 영역과 그 외의 영역을 구분하여 이벤트 처리를 하는 것이었습니다. 다만, 이러한 구분을 위해서는 가장 상단의 영역인 root 요소까지 이벤트 처리가 올라가야한다는 점이었습니다. 이는 컴퍼넌트 단위 개발 관점에서 적합하지 않아보였습니다.
해결 : onBlur
내에서 처리하되, 연관 검색어에 대한 처리를 추가했습니다. 우선 연관검색어가 보여지는지 여부를 관리하는 상태 isOpen
과 현재 페이지에서 검색된 키워드인 actualKeyword
상태를 두었습니다. 작업 과정은 다음과 같습니다.
relatedTarget
의 랜더트리에 특정 클래스가 포함되는지 확인합니다.
- 반응형 - 윈도우 viewport 너비에 따라 조건부 렌더링
- 시맨틱한 구조 - 시맨틱 태그 사용
- 서브메뉴는 헤더의 상단 섹션 바로 하단에 위치 - position 속성 이용
- 재랜더링 최소화 - 헤더의 상단 부분 useMemo
재랜더링 최소화 :
상태에 따른 조건부 렌더링으로 반응형 구현 :
isMobile(boolean)
상태로 현재 모바일 버전인지 데스크탑 버전인지 관리
@congaweb님께서 제작해주셨고, 코드리뷰를 통해 작업 과정의 고민을 공유받을 수 있었습니다. 단순해 보이는 Taglist이지만 리액트의 동작 원리에 대한 이해가 필요한 작업이었습니다.
다음 장에서는 성능 최적화 및 접근성 측면에서 GIFT 팀의 노력을 공유해보고, 총 마무리 회고를 적어보겠습니다!