한 페이지 내에서 각 내용이 있는 위치(조건부 렌더링 ❌)로 이동할 수 있는 tab이 있다. 클릭해도 해당 탭 내용으로 이동해야 하고(기능 1
), 그냥 스크롤로 이동을 해도 focus된 탭이 바뀌어야 함(기능 2
)
기능 1
구현을 위해 useRef와 scrollIntoView를,
기능 2
구현을 위해 useRef와 InterSectionObserver를 사용했다!
특정한 virtual Dom element를 선택하고자 할 때!
input을 강제로 focus 시키거나, 요소의 크기나 위치를 얻어야 할 때 등등... 바닐라JS에서는 getElementById, querySelector와 같은 메서드로 특정 엘리먼트를 선택해 이런 작업을 할 수 있었지만, 가상DOM을 이용하는 React에서는 렌더링이 끝나기 전에는 페이지에 해당 element가 존재하지 않기 때문에 해당 방식을 이용할 수 없다. 대신 useRef를 사용해 변수에 원하는 요소를 담을 수 있음!
- 원하는 변수명으로 useRef() 선언
const inputToFocus = useRef()
- 선택하고자 하는 DOM node(virtual DOM element)에 ref 속성 추가
<h1 ref={inputToFocus}>제목</h1>
- 필요한 곳에서 변수명.current로 꺼내 쓰기!
<button onClick={()=>{inputToFocus.current.focus()} />
먼저 useRef를 선언 & DOM node에 연결해 준다.
처음에는 아래처럼 각각의 tab 내용별로 각각의 useRef를 선언해 따로따로 관리했었다.
[리팩토링 전]
const firstTab = useRef();
const secondTab = useRef();
const thirdTab = useRef();
.
.
.
<TabContents ref={firstTab}>...</TabContents>
<TabContents ref={secondTab}>...</TabContents>
<TabContents ref={thirdTab}>...</TabContents>
근데 멘토님 덕분에 알게 된 새로운 사실! useRef에 그냥 하나의 element가 아니라, 리스트로 여러 개의 element를 넣을 수도 있음! 그래서 유사한 요소들의 경우 리스트로 하나의 변수에 담으면 굳이 useRef를 여러 개 만들지 않아도 된다~~~
[리팩토링 후]
const tabRef = useRef([]);
.
.
.
<TabContents ref={el => (tabRef.current[0] = el)}>...</TabContents>
<TabContents ref={el => (tabRef.current[1] = el)}>...</TabContents>
<TabContents ref={el => (tabRef.current[2] = el)}>...</TabContents>
근데 위처럼 엘리먼트에 연결해 줄 때 그냥 변수명이 아니라, 콜백함수 형태로 넣어줘야 함😮 ref={} 내부에서 함수를 호출하면 인자로 DOM node가 들어온다. 그래서 tabRef.current의 배열에 하나씩 할당해 주는 것!
참고로 useRef를 통해 생성한 변수는 상태값이 변경되어도 즉시 컴포넌트를 리렌더링시키진 않는다. 그래서 ref.current에 HTMLElement뿐만 아니라, 변경사항을 관리해야 하지만 굳이 변경시마다 리렌더링을 발생시킬 필요가 없는 숫자나 배열 등을 할당해 사용할 수 있는 거라고 한다!
그리고 선택된 탭에 스타일을 줄 수 있도록 currentTab이라는 state를 만들고, 각 tab의 onClick 이벤트에서 클릭한 탭으로 이동하도록 + 클릭한 탭이 currentTab이 되도록하는 scrollIntoView와 setCurrentTab을 호출했다.
const [currentTab, setCurrentTab] = useState();
{TABDATA.map((data, index) => (
<Tab
key={index}
onClick={() => {
tabRef.current[index].scrollIntoView();
setCurrentTab(tabRef.current[index]);
}}
selected={tabRef.current[index] === currentTab}
>
{data}
</Tab>
))}
(styled component인 Tab에 props로 selected가 true이면 font-weight, border-bottom 스타일이 적용되도록 한 것!)
⚡ ScrollIntoView
특정 엘리먼트가 있는 위치로 스크롤을 이동시킬 수 있는 자바스크립트의 내장메서드! 인자로 true/false값 또는 option 객체를 받을 수 있다.
- true/false: true의 경우 해당 요소의 제일 윗부분, false의 경우 제일 아랫부분으로 스크롤이 이동한다.
- option 객체: {
block:"start"/"end",-수직 요소에 대한 옵션 (각각 true/false와 같은 효과)
inline: "start"/"left"/"center"/"nearest",-수평 요소에 대한 옵션
behavior: "auto"/"smooth"-스크롤 시 smooth한 효과를 주고 싶을 때
}
🔻 useRef + scrollIntoView를 사용한 결과물
🔻 scrollIntoView({behavior: "smooth"})로 옵션 추가했을 때!
커뮤니티 데이터가 이상한 이유는 아직 목데이터라서...ㅎ.ㅎ (프로젝트ing)
그러고 보니까 커뮤니티도 Q&A로 수정해야겠다,, 지금발견함!!!😮😮
IntersectionObservers는 다음편에....🏃♀️🏃♀️🏃♀️
Using refs in React functional components (part 1) - useRef + callback ref
React useRef의 다양한 활용 방법(mutable object, callback ref와 forwardRef)