문제 상황 및 구현해야 할 것
이런 느낌의 스크롤을 감지해서 상황마다 버튼이 달라지는 컴포넌트를 만들어야 했다.
스타일은 전부 비슷할 듯 하니 맨 처음에 두겠음
import styled, { css } from 'styled-components';
export const StyledButton = styled.button<{ $isScrolling: boolean }>`
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 1000px;
color: white;
background-color: #2f774d;
transition: all 0.3s ease;
font-weight: ${({ theme }) => theme.fonts.semiBold600};
svg {
width: 24px;
height: 24px;
}
${({ $isScrolling }) =>
$isScrolling &&
css`
width: 45px;
height: 45px;
padding: 10px;
font-size: 0px;
cursor: default;
`}
${({ $isScrolling }) =>
!$isScrolling &&
css`
width: 108px;
height: 42px;
gap: 4px;
padding: 9px;
font-size: 14px;
cursor: pointer;
`}
`;
- isScrolling: 스크롤 상태에 따라 스타일을 동적으로 변경한다.
1차 시도
처음 생각한 방식은 window.scrollY를 사용하는 것
import { useEffect, useState } from 'react';
import PencilIcon from '@/assets/pencil.svg';
import { StyledButton } from './ReviewBtn.style';
interface ScrollButtonProps {
onClick?: () => void;
children: React.ReactNode;
}
export const ReviewBtn = ({ onClick, children }: ScrollButtonProps) => {
const [isScrolling, setIsScrolling] = useState(false);
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 0) {
setIsScrolling(true);
} else {
setIsScrolling(false);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<StyledButton onClick={onClick} $isScrolling={isScrolling} disabled={isScrolling}>
<img src={PencilIcon} alt="Pencil Icon" />
{!isScrolling && <span>{children}</span>}
</StyledButton>
);
};
- isScrolling 상태: useEffect로 스크롤 이벤트를 감지하여 isScrolling 상태를 업데이트한다.
- window.scrollY: 현재 스크롤 위치를 기준으로 상태를 변경한다. 스크롤 중이면 버튼이 비활성화되고, 스크롤이 멈추면 버튼이 활성화된다.
- useEffect는 컴포넌트가 렌더링된 후, 스크롤 이벤트를 감지하기 위해 window 객체에 scroll 이벤트 리스너를 추가한다.
window.addEventListener('scroll', handleScroll);
- handleScroll 함수는 사용자가 페이지를 스크롤할 때 호출되며, window.scrollY 값을 기준으로 스크롤 여부를 판단한다. 스크롤이 발생하면 setIsScrolling(true)로 상태를 업데이트하고, 스크롤이 상단에 있으면 setIsScrolling(false)로 상태를 다시 false로 설정한다.
const handleScroll = () => {
if (window.scrollY > 0) {
setIsScrolling(true);
} else {
setIsScrolling(false);
}
문제점
원하는 대로 동작하지 않음, 스크롤이 "화면의 끝에 도달했을 때"만 상태가 변경되는 문제가 발생함
문제 발생 이유
window.scrollY를 사용하여 스크롤 중 상태를 감지하는 식으로 구현했기 때문에 발생한다.
window.scrollY는 페이지의 상단에서부터 얼마나 스크롤되었는지를 나타내는 값이다.
현재 로직에서는 window.scrollY가 0보다 크면 스크롤이 되었다고 판단하여 isScrolling 상태를 true로 설정하고 0이면 상태를 false로 바꾸고 있다. scrollY 값이 페이지 상단에 도달할 때(0)와 하단에 도달할 때에만 특정하게 작동함!
→ 스크롤의 시작과 끝을 감지하는 데에는 적합하지만, 스크롤 중 상태를 감지하기는 어렵다.
이를 해결하려면 스크롤 위치보다는 스크롤 이벤트의 발생 그 자체를 기반으로 상태를 업데이트해야 한다.
문제 해결 방법
스크롤 이벤트가 발생하는 동안 상태가 지속적으로 업데이트되도록 해야 한다.
타이머를 사용한 스크롤 감지가 대표적이다.
스크롤이 발생할 때마다 상태를 true로 설정하고, 일정 시간이 지나도 스크롤이 발생하지 않으면 상태를 false로 변경하는 방식이다. 이렇게 하면 스크롤이 발생하는 즉시 상태가 업데이트되고, 사용자가 스크롤을 멈추면 자동으로 다시 false 상태로 돌아간다.
스크롤은 매우 빈번하게 발생하는 이벤트이다. 스크롤이 발생할 때마다 상태를 매번 변경하면 성능 저하가 일어날 수 있기 때문에 일정 시간이 지난 후에만 "멈췄다"고 판단함으로써 불필요한 상태 변경과 재렌더링을 줄인다.
즉, 스크롤이 멈춘 후 일정 시간 동안 추가로 이벤트가 없을 때만 상태를 업데이트하는 디바운싱(debouncing) 기법의 일종이다.
2차 시도
setTimeout를 사용한 스크롤 감지
import { useEffect, useRef, useState } from 'react';
import PencilIcon from '@/assets/pencil.svg';
import { StyledButton } from './ReviewBtn.style';
interface ScrollButtonProps {
onClick?: () => void;
children: React.ReactNode;
}
export const ReviewBtn = ({ onClick, children }: ScrollButtonProps) => {
const [isScrolling, setIsScrolling] = useState(false);
const scrollTimeout = useRef<number | null>(null);
useEffect(() => {
const handleScroll = () => {
setIsScrolling(true);
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
scrollTimeout.current = window.setTimeout(() => {
setIsScrolling(false);
}, 300);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
};
}, []);
return (
<StyledButton onClick={onClick} $isScrolling={isScrolling} disabled={isScrolling}>
] <img src={PencilIcon} alt="Pencil Icon" />
{!isScrolling && <span>{children}</span>}
</StyledButton>
);
};
setTimeout을 사용하여 스크롤 상태 감지
- 사용자가 스크롤할 때마다 setIsScrolling(true)로 "스크롤 중" 상태를 즉시 설정한다.
- 스크롤이 멈췄는지 감지하기 위해 300ms의 지연 시간을 설정하고, 스크롤이 추가로 발생하지 않으면 "멈춤" 상태로 전환한다.
clearTimeout으로 중복된 스크롤 감지 방지 및 타이머 초기화
- clearTimeout(scrollTimeout)을 사용하여 연속적인 스크롤 이벤트가 발생할 때 기존 타이머를 지워준다.
- 타이머 초기화
- 기존에 설정된 타이머가 있을 경우, clearTimeout(scrollTimeout.current)를 사용하여 타이머를 초기화한다. 이후 새로운 타이머를 설정한 후, scrollTimeout.current에 해당 타이머의 ID를 저장하여 이후에 접근할 수 있게 한다.
useRef 사용
- scrollTimeout을 useRef로 선언하여 리렌더링 시에도 변수가 유지되도록 했다.
컴포넌트 언마운트 시 타이머 정리
- useEffect의 cleanup 함수에서 컴포넌트가 언마운트될 때 타이머가 설정된 상태라면, clearTimeout을 호출하여 타이머를 정리한다.
문제점
ㅈㄴ 쓰면 안될 것 같은 경고창이 개많이 뜸
Violation: 'click' handler took <N> ms
대충 이 핸들러 호출하고 종료되기까지 n초 걸렸으니 왠만하면 비동기로 동작하게끔 하는 게 좋겠다 라는 의미
- 브라우저가 클릭 이벤트 처리에 너무 오랜 시간이 걸리고 있다는 경고!
- 이는 JavaScript의 클릭 핸들러가 메인 스레드에서 너무 많은 일을 하고 있어, 브라우저가 클릭 후의 반응성을 늦추고 있다는 의미이다.
문제 원인
원인은 출력용 alert 함수 때문이었는데, alert로 출력된 경고 메시지의 ‘확인' 버튼을 눌러야 함수가 종료되었다고 판단하기 때문에 확인 버튼을 언제 누르냐에 따라 위의 경고 메시지의 시간이 달라진다.
alert 함수는 자바스크립트에서 경고 메시지를 띄우는 함수로, 사용자가 "확인" 버튼을 누를 때까지 자바스크립트의 실행을 중단시킨다. 이 때문에 발생하는 성능 문제나 클릭 처리 지연이 발생할 수 있다.
- alert는 자바스크립트의 실행을 블로킹하는 방식으로 동작한다.
- 사용자가 "확인"을 누르기 전까지 다음 코드가 실행되지 않는다.
alert 함수에 관련하여
alert 함수는 자바스크립트에서 매우 기본적인 기능으로, 경고 메시지를 빠르게 띄울 때 유용하지만, 모든 자바스크립트 코드의 실행을 일시 중지시키는 문제점이 있다. ⇒ 블로킹 됨! 이는 동기적인 특성 때문에 발생하는데, alert가 실행되는 동안 브라우저의 모든 처리가 멈추고, 사용자가 경고창을 닫을 때까지 페이지가 반응하지 않는다. 이런 이유로 개발 중에는 코드 흐름이 멈춰버리기 때문에 디버깅 시 불편을 초래할 수 있다. 이를 해결하기 위해서는 비동기 처리 방법(예: Promise, setTimeout)을 별도로 사용해야 한다.
에러 레퍼런스
[Violation] took 1000⬆️ms
[Violation] took 1000⬆️ms 이게 뭐지 앞에서 this 바인딩 문제를 해결하니 이번엔 이상한 경고가 출력된다 대충 이 핸들러 호출하고 종료되기까지 1016초 걸렸으니 왠만하면 비동기로 동작하게끔 하
blog.chichoon.com
트러블슈팅 (해결 방법)
비동기 경고창 사용
alert 대신 비동기적으로 동작하는 모달 창을 사용하여 사용자에게 경고를 표시하면서도, 자바스크립트 실행을 차단하지 않는 방법으로 변경한다.
최종 화면
우하하
'React' 카테고리의 다른 글
React에서 웹과 앱 환경에 따라 하단바 다르게 표시하기 (0) | 2025.01.07 |
---|---|
TanStack Router v7로 타입 안전한 라우팅 구현하기 (3) | 2024.11.17 |
[React/리액트] 상태 관리 추천 / Zustand 사용법 (0) | 2024.08.03 |
[React/TS] 주소 검색 기능 구현하기 / 주소를 위도 경도로 변환하기 / 카카오맵 API 타입 정의 (0) | 2024.07.30 |
[React/리액트] 파일 업로드와 미리보기 기능 구현하기 (0) | 2024.07.28 |