[React] 동적으로 생성된 컴포넌트에 Ref를 이용하여 스크롤 이동 구현

SangHyun Park·2024년 9월 18일
0

React

목록 보기
1/3

요약

api 호출을 통해 생성된 특정 컴포넌트로 스크롤 이동을 구현.

useRef와 응답 데이터를 가공해 ref 배열을 만들고 이를 이용하여 특정 dom에 접근한다.

문제

  • 월별 경기일정 페이지에서 오늘 날짜에 해당하는 경기일정 컴포넌트로 스크롤링한다.
  • 오늘 경기일정이 없으면 가장 차이가 나지 않는 경기 일정으로 스크롤링한다. ex) 오늘이 9월 10일 일때 9/1와 9/11 중 9/11을 선택
  • 다른 달 경기일정일때는 스크롤링을 하지 않는다.
  • 데이터는 서버를 통해 받기에 useRef로 전부 선언 할 수 없다.
  • 동적으로 ref를 만들어 해당 dom을 조작
  • Ts와 React 환경

해결

1. 타입선언

type DivRef = {
  ref: HTMLDivElement | null;
};
type TGameListWithDateResponseDate = Pick<TGameListWithDate, 'date'>;

export type TGameListDateRef = TGameListWithDateResponseDate & DivRef;
  • ref타입과, api응답 타입을 통해 특정 타입(Pick)을 선택
  • 두개의 타입을 결합(=확장, &)
  • 날짜를 통해 스크롤링 위치를 결정하기에 date타입을 선언.

2. Ref 선언

// shedule.tsx

const scheduleListRef = useRef<TGameListDateRef[]>([]);

3. DOM 노드의 JSX에 ref 속성 전달

// shedule.tsx

sortedMonthlyGameList.map(({ games: gameList, date }, index) => (
	<ScheduleContent
		date={date}
		gameList={gameList}
		isHome={false}
		key={index}
		ref={(ref) => {
			ref && scheduleListRef.current.push({ date, ref });
		}}
		/>
	))
  • ref 속성에서 DOM ref를 받아 scheduleListRef.current에 날짜(date)와 ref값을 푸쉬
// ScheduleContent.tsx

const ScheduleContent = forwardRef<HTMLDivElement, ScheduleContentProps>(
  ({ gameList, date, isHome }, ref) => {
    return (
      <div
        className="divide-y divide-border-and-divide overflow-hidden rounded-md border border-border-and-divide"
        ref={ref}
      >
        ...
      </div>
    );
  },
);
ScheduleContent.displayName = 'ScheduleContent';
  • 자식 컴포넌트에 ref를 가져오기 위해 forwardRef로 감싸기.

4. scheduleListRef의 값들.

useEffet를 통해 스크롤링

// schedule.tsx

useEffect(() => {
    // 서버에서 가져온 데이터를 통해 만들어진 경기일정 컴포넌트의 ref를 이용.
    // 오늘 날짜와 경기일정들의 date들을 비교 하여. 가장가까운 ref찾는다.
    // 찾은 ref를 통해 스크롤을 이동시킨다.

    const todayDate = getTodayDate();

    if (selectedYearMonthDate.getMonth() !== todayDate.getMonth()) {
      // 다른 날짜는 굳이 스크롤 할 필요 없어서 바로 리턴.
      return;
    }

    if (scheduleListRef && scheduleListRef.current.length > 0) {

      let targetRef: TGameListDateRef = scheduleListRef.current[0];
      let dateDifference = Number.MAX_SAFE_INTEGER;

      for (let i = 0; i < scheduleListRef.current.length; i++) {
        const currentRef = scheduleListRef.current[i];
        if (currentRef.date === shortISO(todayDate)) {
          // 같은 날짜면 더 볼 필요 없음
          targetRef = currentRef;
          break;
        } else {
          // 같은 날짜가 아니라면 차이가 제일 적은 날짜로 설정.
          const tempDiff = Math.abs(new Date(currentRef.date).getTime() - todayDate.getTime());
          if (dateDifference > tempDiff) {
            targetRef = currentRef;
            dateDifference = tempDiff;
          }
        }
      }
      targetRef.ref?.scrollIntoView({ behavior: 'smooth', block: 'center' });

      // ref 초기화
      scheduleListRef.current = [];
    }
  }, [gameList, gameListByTeamId, selectedYearMonthDate]);

참고 자료

useRef object배열로 관리하기 + 리팩토링:

https://80000coding.oopy.io/774fd94b-686d-4a95-aeab-b8f0a376f1c6

useRef 공식 문서 :

https://ko.react.dev/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes

profile
마라토너

0개의 댓글