Conversation
- 탭바 동작 테스트를 위한 임시 컴포넌트들
- App.tsx에 예시 코드 생성 - 해당 섹션에서 탭바 active - 탭 클릭 시 해당 섹션으로 이동 - 스크롤 되다가 헤더와 닿으면 sticky로 고정
빌드 결과빌드 성공 🥳 |
빌드 결과빌드 성공 🥳 |
빌드 결과빌드 성공 🥳 |
odukong
left a comment
There was a problem hiding this comment.
특정 요소 위치로 스크롤 인터렉션하는 기능이 구현하기 까다로우셨을 수 있을 것 같은데,
각 section ref를 observer로 감지하도록 하는 useScrollableTabs 별도의 훅으로 분리하여 컴포넌트 내부 코드도 간결해지고 너무 좋은 것 같습니다 고생 많으셨어요 (/≧▽≦)/
몇 가지 제안사항을 코드리뷰로 남겨놓았는데 참고 부탁드릴게요
| const [activeTab, setActiveTab] = useState< | ||
| "product-info" | "review" | "recommend" | ||
| >("product-info"); |
There was a problem hiding this comment.
useScrollableTabs 내부에서 "product-info" | "review" | "recommend" 문자열 리터럴 타입이 많이 활용되는 것 같아요
해당 타입들은 별도 타입으로 정의하여 사용하는 것이 타입 안정성면에서 좋을 것 같습니당!!
// 타입 정의
export type TabType = "product-info" | "review" | "recommend";
// 타입 활용
const [activeTab, setActiveTab] = useState<TabType>("product-info");
const sectionId = entry.target.getAttribute("data-section") as TabType;There was a problem hiding this comment.
섬세하다 섬세한 섬세하다 섬세한
(놓친 부분 세세히 봐주셔서 감사해요 😍)
| const sectionRefMap = { | ||
| "product-info": productInfoRef, | ||
| review: reviewRef, | ||
| recommend: recommendRef, | ||
| }; |
There was a problem hiding this comment.
마찬가지로 TabType 타입을 활용해 sectionRefMap에 타입을 명시해 주는 게 어떨까요?
const sectionRefMap: Record<TabType, RefObject<HTMLDivElement>> = {
"product-info": productInfoRef,
review: reviewRef,
recommend: recommendRef,
};지금은 일반 객체라 키 값에 오타가 나거나, 혹시나 나중에 탭이 추가되었을 때 누락되면 에러가 날 수 있을 것 같아요. 함께 Record 타입을 사용하면 모든 탭 ID가 키로 포함되었는지 강제할 수 있어서 유지보수에 훨씬 안전할 것 같습니당
| const offsetTop = | ||
| targetRef.current.offsetTop - HEADER_HEIGHT - TAB_BAR_HEIGHT; | ||
|
|
||
| window.scrollTo({ | ||
| top: offsetTop, | ||
| behavior: "smooth", | ||
| }); |
There was a problem hiding this comment.
현재 감지 대상 상단으로 부터 떨어진 Y축의 값을 나타내는 targetRef.current.offsetTop 활용하여 스크롤 되어야 할 값인offsetTop을 구하고 계신데, 이 방식은 추후에 레이아웃이 변경되면 스크롤되는 위치가 틀어지는 이슈가 발생할 수 있다고 해요.
element.offsetTop의 경우, 문서 전체를 기준으로 요소의 top을 가져오는게 아니라 position이 설정된 가장 가까운 부모를 기준으로 top을 계산한다고 합니다.
지금은 부모 요소에 position와 별도의 레이아웃이 따로 설정되어있지 않아 문서 body를 기준으로 계산되어 잘 작동되고 있지만,
<div className={styles.test} style={{ position: "relative", marginTop: "100rem" }}>위 코드처럼 부모 요소에 position을 지정하고 약간의 레이아웃을 변경하면, 아래와 같이 스크롤 이벤트를 발생시켰을 때는 잘 작동이 되지만, 직접 탭을 클릭해 스크롤 이벤트를 발생시키면 스크롤 위치가 틀어지는 이슈가 발생합니다.
-.Clipchamp.mp4
이러한 문제를 방지하기 위해 getBoundingClientRect().top을 활용하는 방식을 제안드립니당!!
getBoundingClientRect().top은 현재 보고 있는 뷰포트의 최상단에서 해당 요소가 얼마나 떨어져 있는지를 계산하기 때문에 부모 요소와는 무관하게 정확한 값을 구할 수 있다고 해요!
그리고 현재 유저가 스크롤 한 y값(window.scrollY)을 함께 더해주면 레이아웃 구조와는 상관없이 절대적인 위치를 기준으로 탭 스크롤이 가능해질 것 같습니다💛
| const offsetTop = | |
| targetRef.current.offsetTop - HEADER_HEIGHT - TAB_BAR_HEIGHT; | |
| window.scrollTo({ | |
| top: offsetTop, | |
| behavior: "smooth", | |
| }); | |
| const offsetTop = | |
| targetRef.current.getBoundingClientRect().top + | |
| window.scrollY - | |
| HEADER_HEIGHT - | |
| TAB_BAR_HEIGHT; |
There was a problem hiding this comment.
예시까지 보여주셔서 바로 이해가 됐습니다!!!!! 😍
offsetTop이 문서 전체 기준이 아니라 position이 잡힌 부모 요소 기준이라는 점을 몰랐었는데, 그래서 지금은 부모에 별도 레이아웃이 없어서 우연히 정확해 보였던 거군요...
순간 그럼 IntersectionObserver는 왜 정상이지??를 고민했는데, IntersectionObserver는 실제 DOM의 top 값을 사용하는 게 아니라 브라우저가 계산한 뷰포트에 보이는지 여부만 판단해서 문제 없이 동작했던 거였네요
반면 탭 클릭 시에는 offsetTop 값이 부모 기준으로 계산되니까, 실제 문서에서의 위치와 어긋나서 스크롤이 잘못된 지점으로 이동했던 것이고요 🤔
덕분에 아주 확실히 이해했습니다! 말씀해주신 방향으로 바로 수정하겠습니다 :) 감사합니닷 ㅎㅎㅎ
빌드 결과빌드 성공 🥳 |
📌 Summary
사용자 스크롤 감지 탭바 컴포넌트를 구현했습니다.
📄 Tasks
아래 요구사항에 맞춰 ui와 기능을 구현했습니다!
(1) UI (요구사항 1 해결)
position: sticky + top: 5.6remborderBottom + margin 보정1px border-bottom을 가지고 있고 각 탭 버튼은 선택된 경우2px border-bottom을 가지는데, 이때 자연스럽게 활성 탭만 아래에 강조된 언더라인이 생겨 보이도록 하기 위해marginBottom: -1px로 조정하여 버튼 border가 TabBar의 border를 덮어버리는 형태를 만들었습니다!!(2) 기능 (useScrollableTabs 훅 / 요구사항 2와 3 해결)
hook으로 분리했기 때문에 사용할 때는 아래와 같이
🔍 To Reviewer
기존 방식은 window scroll 이벤트가 발생할 때마다 매번 offsetTop을 계산해 현재 보고 있는 섹션을 판별하도록 구현되어 있었는데, 탭을 클릭해 스크롤이 이동될 때 activeTab 업데이트 타이밍이 미묘하게 늦으면서 활성 탭 전환이 자연스럽지 않거나 순간적으로 이전 탭을 유지하는 플리커 현상이 보였습니다. 이 문제 외에도 스크롤 중에 이벤트가 수백~수천 번 호출될 수 있어 계산 비용이 반복적으로 발생할 수 있음을 고려해 IntersectionObserver를 사용해 브라우저가 직접 섹션의 가시 상태를 감지하도록 변경했습니다.
섹션이 화면 중앙 근처에 위치할 때 감지되도록 rootMargin을
-window.innerHeight / 2기준으로 지정해, 사용자가 실제로 해당 섹션을 보고 있다고 느끼는 시점에 activeTab이 전환되도록 했습니다.이 덕분에 탭 클릭 후 스크롤 애니메이션 중에도 정확한 시점에 탭 활성화가 이루어지며, 스크롤 위치 계산 없이도 안정적으로 감지됩니다!
또한 스크롤 이벤트 polling 방식이 사라지면서 렌더링 비용이 줄어 성능 측면에서도 유리할 듯 싶습니다.
📸 Screenshot
2025-11-23.12.12.29.mov