[리팩토링]
useRef로 반응형 resize 이벤트 구현하기
모바일에 최적화된 프로젝트를 기획했지만, 모바일이라고 모든 기기가 동일한 크기를 가지고 있지는 않다. 여러 기기에 맞춰 특별히 어디가 모나지 않은 스타일링을 하기 위해서는 반응형 웹페이지로 구현하는 것이 중요하다. 주변 프론트 개발자분들이 반응형 때문에 어려움을 겪는 것도 보았기 때문에 중요하게 생각하고 있는 문제였다. 이번 리팩토링 시간에는 미디어쿼리를 이용해 실시간으로 완벽한 반응형 페이지를 구현하는 것은 아니지만, 리액트와 자바스크립트를 이용해 resize 이벤트를 만들어보려 한다. 추가로 쓰로틀링 기법을 이용해 최적화까지 진행할 것이다.
1. 기존의 상태
일기 작성 및 수정 페이지와, 타임라인 페이지 두 곳에서 해당 문제가 일어났다. 화면의 width 사이즈를 줄이면 나머지 요소는 제대로 줄어드는데, 감정 아이콘들이 변함없이 한결같은 모습을 하고 있다. 타임라인 페이지에서도 감정 아이콘에서 동일한 현상이 발생했다. 이미지는 문제가 발생한 스크린샷을 남겨두지 않아서 급조한 예시 이미지이다. (그래서 이미지 url을 제대로 불러오지 못하고 있다)
2. 반응형 구현
화면의 가로 사이즈가 변경되면 그에 맞춰 감정 아이콘의 크기가 변경되도록 리팩토링했다. React의 useRef 훅과, 중복 요청을 줄여 성능을 최적화하는 throttle 기법을 사용했다. 사이즈 변경에 즉시 반응하는 완벽한 반응형은 아니지만, 이 프로젝트는 모바일 환경에 최적화되도록 기획했고 모바일 기기는 대부분의 경우 가로 사이즈가 고정된 채로 사용하기 때문에 이런 방식으로 구현했다. 브라우저로 서비스를 이용하는 경우 임의로 브라우저의 사이즈를 조정할 수는 있겠으나, 그에 맞춰 어느정도 커버할 수 있다.
3. useRef로 width 값 받아와 height 변경하기
📌 핵심
1. useEffect를 이용해 브라우저가 렌더링될 때와, ref가 변경될 때의 clientWidth 값으로 width 상태 값을 업데이트한다.
2. width 값을 받아 height 값을 변경시킨다.
useRef로 새로운 ref를 생성한다. div 요소인 Container에 ref를 연결한다. 이때 ref를 콘솔에 출력하면, ref 객체의 값이 저장되는 current 프로퍼티를 볼 수 있다. ref.current로 접근할 수 있는 DOM 요소 중에는 clientWidth 등이 있다. 이번에 활용할 요소는 바로 이 clientWidth이다.
width 값을 저장하고 관리해야하기 때문에 useState를 선언해준다. 화면 크기가 변경되면 그에 맞게 감정 아이콘의 크기도 변경되야 하기 때문에, containerRef의 값이 변경될 때마다 리렌더되는 useEffect를 활용한다. 변경된 크기를 받아와 setWidth를 이용해 값을 업데이트한다. 이 값을 아이콘 요소의 height에 props로 전달한다.
export function EmotionItem({ emotion, imgSrc, isSelected, onClick }: EmotionItemProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
if (containerRef.current) {
console.log(containerRef.current?.clientWidth);
setWidth(containerRef.current?.clientWidth || 0);
}
}, [containerRef]);
return (
<Container onClick={() => onClick(emotion)} ref={containerRef}> // ✅
<EmotionHeader isSelected={isSelected} height={width}> // ✅
<img src={imgSrc} alt={emotion} />
</EmotionHeader>
<EmotionBody>{emotion}</EmotionBody>
</Container>
);
}
const EmotionHeader = styled.div<{ isSelected: boolean; height: number }>`
width: 100%;
height: ${(props) => props.height}px; // ✅
// ...
`;
여기까지 우선적인 resize 반응형 작업이 끝났다.
4. throttling 활용해 성능 개선하기
useEffect 안에서 콘솔에 ref.current.clientWidth를 출력하면 화면의 크기를 변경할 때마다 콘솔이 여러번 찍히는 것을 확인할 수 있다. 사이즈를 계속 변경하면서 사용하는 것이 아니기 때문에 요청이 이렇게 많이 일어날 필요는 없다. 그래서 쓰로틀링 코드를 추가해 불필요한 함수 호출을 줄여볼 것이다.
throttle은 함수가 여러번 중복 실행되는 것을 줄이기 위해, 일정 시간에 한 번만 실행되도록 하는 기법이다. 실행 횟수에 제한을 두는 것이다. 디바운스와 다른 점이 이 지점이다. 디바운스는 중복으로 호출해도 마지막에 딱 한 번 실행되는 반면, 스로틀은 일정 간격으로 호출이 발생한다.
const throttle = (callback, ms) => {
let timer; // 나중에 쓰로틀링된 함수가 실행될 때 사용됨, null 또는 undefined인 경우에만 함수가 실행
return (...args) => { // 인자로 ...args를 받아 나중에 실행될 callback 함수에 전달
if (!timer) { // 쓰로틀링된 함수가 이미 예정된 상태에서 호출되었을 때 중복 실행되지 않도록 함
timer = setTimeout(() => { // 주어진 시간(ms) 이후에 내부 콜백 함수를 실행
timer = null; // timer 변수를 null로 설정하여 다음 호출을 위해 초기화
callback(...args); // 콜백 함수 실행
}, ms);
}
}
};
쓰로틀 코드이다. 예전에 디바운스를 학습하면서 한 줄 한 줄 이해가 되지 않아 고생했던 기억이 있기 때문에 쓰로틀도 매 줄마다 주석을 달아 공부했다. 이렇게 해둬야 잊어버렸다가 나중에 다시 봐도 이해가 수월하다. 리액트와 타입스크립트를 사용하는 프로젝트에서는 좀 손을 봐서 아래와 같이 완성했다.
const throttle = (callback: (...args: any[]) => void, ms: number): ((...args: any[]) => void) => {
let timer: NodeJS.Timeout | null = null;
return (...args: any[]) => {
if (!timer) {
timer = setTimeout(() => {
timer = null;
callback(...args);
console.log(window.innerWidth);
}, ms);
}
};
};
인자로 받는 callback은 실행을 제한하려는 함수이다. ms는 스로틀링 간격을 나타내는 ms의 시간이다. 이 시간 간격 내에는 중복으로 실행되지 않도록 한다.
5. resize 이벤트 등록하기
3에서 구현해놓은 로직을 handleResize 함수로 분리했다. 4에서 만든 throttle 함수에 콜백으로 이 함수를 전달한다. useEffect애서는 초기 렌더링 시에 리사이즈 함수가 실행되도록 하고, addEventListener로 이벤트를 추가해준다. 추가한 이벤트를 잊지 않고 removeEventListener로 지워주면 된다. 필요에 따라 쓰로틀 함수를 유틸 함수로 분리하고, 유닛 테스트 코드를 작성할 수 있다. 이렇게 하면 가로 사이즈가 변경될 때 반응형으로 동작하는 스타일링을 할 수 있다.
const handleResize = useCallback(() => {
if (containerRef.current) {
setWidth(containerRef.current?.clientWidth || 0);
}
}, [containerRef]);
const handleThrottleResize = useCallback(throttle(handleResize, 800), [handleResize]);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleThrottleResize);
return () => {
window.removeEventListener('resize', handleThrottleResize);
};
}, [handleResize]);
}
마치며
디바운스는 검색 기능을 구현할 때 종종 사용했으나, 쓰로틀링 기법을 사용하는 것은 처음이었다. 비슷한듯 다른 두 기법을 잘 숙지하고 있으면 프로젝트를 효율적으로 개선할 수 있다. 스타일링에서 크게 어려움을 겪을거라고는 생각해보지 않았는데, 반응형으로 만들기는 이리저리 CSS를 만져봐도 잘 해결되지 않은 부분이었다. 결국 자바스크립트를 이용하는 방법을 사용했다. ref로 접근할 수 있는 DOM 요소에 clientWidth 등이 있는줄 몰랐는데 이번 기회에 알게 되었다. 사실 useRef를 그다지 사용해오지 않았기 때문에, 이번 반응형 구현이 ref와 쓰로틀을 동시에 학습할 수 있는 좋은 경험이었다.
'프로젝트 > 하루한냥' 카테고리의 다른 글
[하루한냥] 심리 테스트 구현 : API 호출 함수 분리하기 (1) | 2023.10.02 |
---|---|
[하루한냥] 심리 테스트 구현 : 라디오 버튼 구현하기 (0) | 2023.10.02 |
[리팩토링] 디렉토리별 index를 이용한 모듈화 작업하기 (0) | 2023.09.15 |
[트러블 슈팅] 타입스크립트에 Context와 Promise를 싸서 드셔보세요 (0) | 2023.09.05 |
[하루한냥] 모달 모듈화 구현 : OverlayProvider와 Context API (0) | 2023.09.02 |