전역으로 ref 공유하는 방법(with Jotai and without useRef)

김철준·2024년 7월 21일
2

REACT

목록 보기
34/38

하나의 페이지가 여러 컴포넌트로 나누어져있을 때, 특정 컴포넌트를 참조해야하는 경우가 있다면 어떻게 해야할까?

요구사항은 다음과 같다.
특정 컴포넌트들이 페이지 내 특정 영역에서 position이 sticky 방식으로 움직이다가 특정 영역에 들어서면 sticky가 해지되야한다.

위 동영상에서 확인했을 때, 위에서부터 아래로 내려오는 3개의 빈 컨테이너들이 sticky로 잡혀있다가 전문기업이라는 텍스트가 적힌 영역에 들어서면 sticky가 해지된다.

위 3개의 빈 컨테이너들은 각각 3개의 컴포넌트들로 나눠져있는 상태이다.

sticky로 설정해놔야하는 3개의 컴포넌트들이 그러면 3번째 sticky 컨테이너가 스크롤 끝에 닿았을 때, sticky가 해지되야한다.

위 폴더 구조는 페이지를 구성하고 있는 폴더 구조이다.
페이지 내에 컨텐츠들이 다양하기 때문에 여러 컴포넌트로 나누었다.
디렉터리로 구분되어있는 것들이 하나의 영역이면서 하나의 컴포넌트로 잡혀져있다.

페이지를 구성하는 컴포넌트

  • topScreen
  • descriptionContents
  • expertCompany
    ~~
    ~~
  • specializedList

위에서 sticky로 잡혀있어야하는 컨텐츠는

  • descriptionContents
  • expertCompany
  • goodPointsList

이 3개이다.
이 3개는 마지막 3번째 sticky contents인 goodPointsList 컴포넌트가 스크롤 끝에 다다르면 sticky가 해지되어야한다.

그래서 내가 생각한 방법은

  • sticky contents들의 positon을 기본적으로 sticky로 설정을 한다.
  • sticky가 되지 않아야하는 영역(nonStickyArea라고 하겠다.)을 ref로 지정한다.

이 방법이다.

nonStickyArea를 이루는 컴포넌트들은 다음과 같다.

  • inquiryList
  • portpolios
  • serviceList
  • specializedList

그렇다면 nonStickyArea를 참조할 수 있어야한다.

react나 nextjs에서 특정 요소를 참조하기 위해서는 useRef라는 것을 사용한다.

useRef(공식문서)

코드는 크게 다음과 같이 구성되어있다 생각하면 된다.

page.tsx

import React from "react";
import DescriptionContents from "@/app/(page)/main/bottomContents/descriptionContents/descriptionContents";
import BottomSecondContents from "@/app/(page)/main/bottomContents/expertCompany/expertCompany";
import GoodPointsList from "@/app/(page)/main/bottomContents/goodPointsLIst/goodPointsList";
import TopScreen from "@/app/(page)/main/topScreen/topScreen";
import CommonLabel from "@/app/(page)/main/ui/commonLabel";
import NonStickyArea from "@/app/(page)/main/ui/nonStickyArea";

const Page = () => {
  return <div className={"text-achromatic-white mb-[100px]"}>
    {/*검색 컨테이너 */}
    <TopScreen/>
  {/*  이하 하단 영역*/}
    <div className={"lg:px-[320px] md:px-[40px] sm:px-[8px] lg:mt-[160px] md:mt-[120px] sm:mt-[61px]"}>
      <CommonLabel>AI 매칭</CommonLabel>
      {/*STICKY AREA*/}
      <DescriptionContents/>
      <BottomSecondContents/>
      <GoodPointsList/>
      {/*NONSTICKY AREA*/}
      <NonStickyArea/>
    </div>
  </div>;
};

export default Page;

NonStickyArea.tsx

"use client"

import React from "react";
import SpecializedList from "@/app/(page)/main/bottomContents/specializedList/specializedList";
import PortPolioList from "@/app/(page)/main/bottomContents/portpolios/portpolioList";
import ServiceList from "@/app/(page)/main/bottomContents/serviceList/serviceList";
import InquiryList from "@/app/(page)/main/bottomContents/inquiryList/inquiryList";
import { useAtom, useAtomValue } from "jotai/index";
import { nonStickyAreaRefAtom } from "@/app/_store/main";

const NonStickyArea = () => {

  const nonStickyRef= useAtomValue(nonStickyAreaRefAtom);

  return (
    <div
    ref={nonStickyRef}
    >
      {/*전문 분야*/}
      <SpecializedList />
      {/*기업 포트폴리오*/}
      <PortPolioList />
      {/*comma-x 제공 서비스*/}
      <ServiceList />
      {/*자주 묻는 질문*/}
      <InquiryList />
    </div>
  );
};

export default NonStickyArea;

Ref를 전역으로 공유하는 방법

그러면 위 NonStickyArea를 ref를 통해 참조해야하고 해당 ref는 모든 컴포넌트들에서 사용해야하기때문에 공유가 가능해야한다.

공유를 할 수 있는 방법은 2가지가 있다.

  1. 제일 위 컴포넌트인 page 컴포넌트에서 아래로 뿌려준다.
  2. 전역상태를 이용하여 ref를 공유한다.

1번의 경우, page.tsx에서 선언하여 아래로 쭉쭉 뿌려줘야하는데 뿌려주는 과정이 나는 나름 귀찮다고 생각이 들었다.
따라서 2번의 방법을 선택하였는데 해당 프로젝트의 경우, 전역 상태 라이브러리로 jotai라는 라이브러리를 사용하고 있다.

jotai

전역 상태로 ref 관리 with 전역 상태 라이브러리

그렇다면 ref를 전역값으로 설정해야한다.

_store 디렉터리(프로젝트에서 전역상태를 관리하는 디렉터리)에서 main.ts라는 파일을 만든 뒤, useRef값을 전역으로 설정해보자

main.ts

import { atom } from "jotai";

export const nonStickyAreaRefAtom=atom(useRef(null)); 

하지만 위와 같이 사용하면 아래와 같은 에러가 발생한다.

ESLint: React Hook "useRef" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.(react-hooks/rules-of-hooks)

react hook은 hook(use~~)이나 컴포넌트내에서 사용해야하는 규칙이 있다.

따라서 위 방법과 같이 사용할 수 없다.

그렇다면 전역으로 ref를 사용할 수 있는 방법은 또 뭐가 있을까?

전역상태 라이브러리와 같이 사용하면 useRef처럼 사용가능하다.
useRef 선언 코드


    interface MutableRefObject<T> {
        current: T;
    }

    function useRef<T>(initialValue: T): MutableRefObject<T>;

main.ts

import { atom } from "jotai";


export const inquiryListRefAtom = atom({current:null});

InquiryList.tsx

const InquiryList = () => {

// const ref ={current:null}

 const ref =  useAtomValue(inquiryListRefAtom)

  console.log("InquiryList ref", ref.current);
  return (
    <div
      ref={ref}

정확한 내부 동작은 잘 모르겠지만 전역상태 초깃값으로 {current:null}값을 선언해주고 해당 값을 참조할 요소에 할당하면 참조가 가능하다.

전역상태 사용하지 않고 ref를 사용하면?

하지만 전역상태를 사용하지 않고 ref를 위와 같이 사용하면

InquiryList.tsx

const InquiryList = () => {

// const ref ={current:null}

 const ref =  useAtomValue(inquiryListRefAtom)

  console.log("InquiryList ref", ref.current);
  return (
    <div
      ref={ref}

요소가 참조가 안되는 것을 확인할 수 있다.

전역 상태 ref를 nonStickyArea에 할당

위와 같이 전역 상태와 같이 ref를 사용하여 nonStickyArea 컴포넌트에 할당하면 nonStickyArea를 참조가능하므로 3개의 stickyContents가 nonStickyArea를 감지했을 때, sticky가 해지되도록 해주면 된다.

nonStickyArea.tsx

"use client"

import React from "react";
import SpecializedList from "@/app/(page)/main/bottomContents/specializedList/specializedList";
import PortPolioList from "@/app/(page)/main/bottomContents/portpolios/portpolioList";
import ServiceList from "@/app/(page)/main/bottomContents/serviceList/serviceList";
import InquiryList from "@/app/(page)/main/bottomContents/inquiryList/inquiryList";
import { useAtom, useAtomValue } from "jotai/index";
import { nonStickyAreaRefAtom } from "@/app/_store/main";

const NonStickyArea = () => {

  const nonStickyRef= useAtomValue(nonStickyAreaRefAtom);

  return (
    <div
    ref={nonStickyRef}
    >
      {/*전문 분야*/}
      <SpecializedList />
      {/*기업 포트폴리오*/}
      <PortPolioList />
      {/*comma-x 제공 서비스*/}
      <ServiceList />
      {/*자주 묻는 질문*/}
      <InquiryList />
    </div>
  );
};

export default NonStickyArea;

아래는 sticky가 되야하는 컨텐츠이다.
sticky가 nonStickyArea를 만나면 해지되고 떨어지면 sticky가 되는 로직은 useControlSticky에서 구현되어있다.

descriptionContents.tsx(StickyContents)

"use client";

import React, { useRef } from "react";
import useControlSticky from "@/app/(page)/main/_utils/useControlSticky";

/**
 * 하위 영역 컨텐츠를 감싸는 컨테이너
 * @constructor
 */
const ContentsWrapper = ({children}:{
  children?: React.ReactNode;

}) => {
  const wrapperRef = useRef(null);
  const{isSticky}= useControlSticky({wrapperRef});
  return (
    <div
      ref={wrapperRef}
      className={`${isSticky ? 'sticky top-[130px]' : ''} z-10 bg-achromatic-darkSub rounded-[32px] w-full lg:h-[680px] lg:my-[150px] md:my-[100px] sm:my-[50px] flex lg:gap-[40px] lg:flex-row flex-col-reverse`}>
      {children}
    </div>
  );
};

export default ContentsWrapper;

useControlSticky.ts

import React, { useEffect, useState } from "react";
import { useAtom, useAtomValue } from "jotai/index";
import { nonStickyAreaRefAtom } from "@/app/_store/main";

const useControlSticky = ({wrapperRef}:{
  wrapperRef: React.MutableRefObject<null>;
}) => {
  // nonStickyArea 요소 참조
  const nonStickyRef= useAtomValue(nonStickyAreaRefAtom);

  const [isSticky, setIsSticky] = useState(true); // sticky 여부

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsSticky(!entry.isIntersecting);
      },
      {
        root: null, // viewport is the default root
        rootMargin: '0px',
        threshold: 0.1, // Trigger as soon as 10% is visible
      }
    );

    if (nonStickyRef.current && wrapperRef.current) {
      observer.observe(nonStickyRef.current);
    }

    return () => {
      if (nonStickyRef.current) {
        observer.unobserve(nonStickyRef.current);
      }
    };
  }, [nonStickyRef]);

  return {
    isSticky
  }
};

export default useControlSticky;

nonStickyArea와 stickyArea를 ref로 참조하고 이를 observer 기능을 사용해서 sticky 상태값으로 관리하는 방법이다.

전역 상태와 함께라면 without useRef

어쨋든 이글에서 제일 중요한 point는 useRef를 사용하지 않아도 전역 상태 라이브러리 JOTAI(다른 전역 상태 라이브러리는 잘 모르겠다.)는 초기값에 {current:null}를 담아주면 ref 값으로 사용할 수 있다는 것이다.

profile
FE DEVELOPER

0개의 댓글