0627 - 0703
0627
트리플 사전과제
이번 트리플 과제를 진행하며 겪었던 문제 해결 과정이나 내 생각을 정리하고자 글을 쓰게 되었다.
사용 기술과 선택한 이유
- React - 과제의 필수 기술 스택. 리액트는 화면 개발에 좋은 도구이다
- Vite - 이전까지 CRA나 직접 웹팩을 구성하여 React를 사용했었다면, 이번 기회에 Vite를 경험해보고 장점을 느끼고자 이를 선택했다. CRA에 비해 소스 코드 갱신이나 개발 서버를 시작하는 데 확실히 빠르다는 장점을 직접 체감할 수 있었다.
- TypeScript - 안정적인 타이핑을 통해 좋은 품질의 코드를 작성하고, 인텔리센스가 제공하는 자동완성 기능을 활용해 개발을 용이하도록 하기 위해 사용했다.
- Styled-Components - 컴포넌트 파일이 과하게 커지지 않는다면 스타일 컴포넌트를 하나의 파일에 둠으로써, 코드 로직을 찾기 용이하고 유지보수하기 용이하다고 생각하여 이를 사용했다. 또한 props로 전달해 주는 값을 쉽게 스타일에 적용함으로써 개발에 용이하다.
문제점과 해결 과정
린팅 셋업 문제
프로젝트 린팅과 포매팅 셋업 시 트리플에서 제공하는 설정을 적용해야 했다.
처음에는 프로젝트를 cra로 구성했었는데, cra 환경에서 해당 설정을 적용하면 제대로 실행되지 않는 오류가 발생했다.
혹시나 해서 해당 리포지토리의 이슈를 확인해봤는데, cra로 생성한 프로젝트에서 의존성을 찾지 못하는 이슈가 존재하는 것을 확인할 수 있었다.
아마도 cra에서 기본으로 설치하는 eslint와 eslint-config-triple에서의 eslint가 충돌이 발생해서 일어난 이슈라고 추론했다.
필요한 추가 의존성을 설치함으로써 문제를 해결할수도 있었겠지만, 정확한 이유를 분석하지 않고 끼워맞추기 식으로 문제를 해결하는 것은 단기적으로 봤을 때는 문제 해결이지만 장기적인 관점으로 봤을 때는 더 큰 문제를 불러일으킬 수 있다고 판단했다.
그래서 이전에 들어만 봤던 vite를 사용해 리액트 프로젝트를 생성해보면 어떨까 생각하여 해당 방식을 사용해 프로젝트를 진행했다.
그 결과 vite로 생성한 프로젝트에서는 정상적으로 eslint가 잘 동작하였고, 추가적으로 vite의 여러 매력들을 직접 느낄 수 있었다.
vite 장점
- 사전 번들링 기능을 Go로 작성된 Esbuild를 사용함으로써, Webpack과 같은 기존 번들러 대비 10-100배 빠른 번들링 속도를 내어 서버 구동이 매우 빠르다
- 소스 코드 업데이트 시 ESM을 이용한 HMR을 지원하여 앱 사이즈가 커져도 빠르게 갱신된다.
다시 확인해보니 현재는 Issue에 임시 해결책이 올라와 있는 상태이다. 다만 내가 과제를 진행했을 때는 해결책이 별도로 올라와있지 않아서, 다른 방법으로 프로젝트 생성을 하는 것으로 우회하여 문제를 해결했다.
컴포넌트 설계 고민
트리플의 평가 항목 중 하나는 컴포넌트를 어떻게 나누는지에 대한 평가였다.
이전에는 재사용만을 기준으로 컴포넌트를 구분하였다면, 이번에는 좀 더 학습하여 나름의 기준을 가지고 컴포넌트를 분리하는 데에 도전해보겠다는 마음가짐으로 과제에 임했다.
보통 기업의 사전 과제를 진행하게 되면, 단순히 주어진 기능만 구현하는 것이 아니라 어떤 부분을 더 깊게 고민하여 진행할 지에 대해 설계하고, 그 부분에 대해 학습하는 시간을 총 과제 기간의 50% 이상 잡으려고 한다. 이렇게 수행하면 설사 과제 전형에서 통과하지 못한다고 하더라도, 개발자로서 성장이 있다고 느껴져 의미 있는 시간을 보냈다는 기분이 든다.
이번 과제에서는 컴포넌트를 어떻게 분리할지에 대한 것이 가장 큰 주제였다.
그렇기에 나보다 더 경험이 많은 FE 개발자들은 어떤 기준으로 컴포넌트를 분리할까 궁금해졌고, 당근 마켓과 토스의 웨비나를 시청하며 그들의 생각을 알아보고자 했다. 웨비나를 시청하면서 좋은 컴포넌트를 설계하는 몇 가지 원칙들을 배우게 되었고, 이번 과제에 해당 원칙들을 내 생각과 함께 녹여 적용해보고자 했다.
이번 과제에서 컴포넌트를 설계하는 데 적용한 원칙들은 다음과 같다.
- 비슷한 관심사라면 가까운 곳에 위치시키기
- 한 가지 역할만 하기
- Headless UI 기반의 추상화하기
컴포넌트에서 필요한 타입과 스타일 컴포넌트를 같은 파일에 위치시킴으로써, 해당 컴포넌트와 관련된 타입과 스타일 코드를 관리하고 유지보수하기에 용이하다. 만약 파일의 코드가 너무 길어지게 된다면, 같은 폴더 내에 다른 파일로 분리하는 방법으로 코드를 관리할 수 있다.
컨테이너와 리스트 아이템 요소로 이루어지는 컴포넌트의 경우, 같은 파일 내에서 분리하여 컴포넌트를 구성하였다. 컨테이너는 전체적인 레이아웃과 useFadeIn hooks를 사용하여 해당 컴포넌트가 언제 보여질지 결정하는 역할을 하고, 아이템은 요소 별 데이터를 기반으로 렌더링하는 역할을 한다.
hooks에서 반환된 값을 어떻게 보여줄지만 정의하면 된다.
그리고 Headless UI 기반의 추상화를 실행하여 데이터를 커스텀 훅으로 분리하였다. 또 다른 등장 애니메이션을 가지거나, 숫자가 올라가는 애니메이션을 가진 컴포넌트를 만들어야 할 때, 디자인이 다르더라도 커스텀 훅을 가져와서 사용할 수 있다. UI를 관심사에서 제거함으로써, 데이터에만 집중해서 모듈화 할 수 있다.
Fade In 기능 구현
처음에 Fade In 기능(살짝 떠오르고, 투명도가 조절되는 애니메이션)을 구현할 때는 keyframes를 사용하여 애니메이션으로 구현한 뒤, HomePage에서 하위 3개의 컴포넌트에게 애니메이션을 동작시키도록 구현했다.
다만 이렇게 구현하게 된다면, 처음에 빈 화면에서 시작하는 것이 아닌 0.1초 뒤에 다음 컴포넌트가 없어졌다 다시 나타나는 방식이었기에 수정할 필요성을 느꼈다. 이는 트리플 홈페이지의 UX와는 다르다고 판단하여 delay 타임을 매개변수로 받는 useFadeIn hooks를 구현한 뒤, opacity와 translateY 값이 showComponent 상태에 따라 transition 속성을 통해 변화하도록 구현했다.
여기서 새롭게 알게 된 사실은 모든 컴포넌트의 Opacity를 0으로, 즉 화면에 보이는 요소가 존재하지 않는다면 첫 로딩 화면이 늦게 보이는 문제가 존재한다는 사실이다. 즉, transition을 활용하면서 좀 더 좋은 UX를 주기 위해서는 과제로 주어진 3개의 컴포넌트 외에 화면에 보이는 요소가 존재해야 더 빠르게 첫 화면이 렌더링 된다고 생각했다. 그래서 투명한 인라인 텍스트를 입력하는 임의의 HiddenBox 컴포넌트를 생성하여 이러한 문제를 해결했다.
물론 실제 트리플 홈페이지를 구현한다면 과제로 주어진 Section 외에도 기존에 보이는 요소들이 존재하기에 문제가 발생하지 않겠지만 현재 상황에서는 이러한 방법으로 UX를 향상시킬 수 있었다.
0630
트리플 사전 과제 보완
이번 트리플 과제 전형에 합격하여 과제와 관련하여 해결되지 못한 고민들을 생각해보고자 글을 작성하게 되었다.
컴포넌트 설계 위주로 공부하는 데에 시간을 많이 소요하여 몇몇 부분은 부족한 채 코드를 제출해야만 했는데, 운이 좋게도 과제 전형에 통과할 수 있었다.
그만큼 부족한 부분들을 잘 보완하여 좋은 모습을 보일 수 있도록 대비하고자 한다!
수정할 부분
useFadeIn
useFadeIn hooks에서 useEffect 내부에 setTimeout으로 타이머를 작동시켰는데, 타이머 해제를 해 주지 않았다.
import { useEffect, useState } from "react";
const useFadeIn = (delay: number) => {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
const timeoutId = setTimeout(() => {
setShowComponent(true);
}, delay);
return () => clearTimeout(timeoutId);
}, []);
return [showComponent];
};
export default useFadeIn;
다음과 같이 코드를 작성해야 버그를 방지하고, 정확하게 원하는 시점에 렌더링 되는 hooks로 사용할 수 있다.
useCountUp
const useCountUp = ({ start = 0, end, duration = 2000 }: Count) => {
const [count, setCount] = useState(start);
const requestRef = useRef(0);
const starttime = useRef(0);
const animate = (timestamp: number) => {
if (!starttime.current) starttime.current = timestamp;
const runtime = timestamp - starttime.current;
if (runtime >= duration) {
setCount(end);
return;
}
setCount(Math.floor(easeOutExpo(runtime, start, end, duration)));
requestRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
return [count];
};
- 기존의 코드는 easing 애니메이션을 적용할 유틸 함수명이 easeInExpo였다. 이는 실제로 동작하는 천천히 느려지는 애니메이션과 다르기 때문에 올바른 작명으로 변경하였다.
- countUp 애니메이션을 구현하기 위해 requestAnimationFrame을 활용하였다. requestAnimationFrame은 프레임 생성 초기 단계에 맞춰 애니메이션 코드를 실행시킴으로써 1프레임 당 호출을 보장한다. 즉, 모니터 주사율에 따라 애니메이션 코드를 실행시키고, 사용자에게 부드러운 애니메이션 동작을 제공하는 데 유용한 함수이다. requestAnimationFrame를 사용할 때, 재귀로 호출하여 사용했어야 했는데 그렇지 않고, 잘못된 방식으로 requestAnimationFrame를 사용했기에 animate에서 재귀적으로 호출하도록 코드를 수정하였다.
- requestAnimationFrame에 전달하는 콜백함수에는 기본적으로 timestamp라는 performance.now()와 같은 값을 반환하는 인자가 전달되는데, 이를 활용하여 animate를 반복적으로 호출할 시간을 결정했다. ref에 animate가 처음 호출된 시간을 저장한 뒤, timestamp에서 그 값을 빼면 animate가 반복 호출된 시간을 구할 수 있었다. 그 값을 duration과 비교하여 requestAnimationFrame이 끝날 조건을 계산하였다.
- 이와 같은 방법으로 최종적으로 코드를 예상한 것과 동일하게 구현할 수 있었다.
과제를 수행하며 배운 점
- 컴포넌트 설계 원칙
- requestAnimationFrame을 리액트에서 잘 사용하는 법
- 리액트에서 css 애니메이션과 js 애니메이션에 대한 고민
- vite의 매력