리액트 이해하기] 05. useRef훅 구현하기

Song-Minhyung·2024년 1월 5일

리액트 이해하기

목록 보기
5/8
post-thumbnail

0. 시작하기 앞서

실제 리액트의 코드는 이와 다릅니다.
제가 작성한 코드는 학습하기 위해 작성한 코드임을 미리 알립니다!
전체 코드는 깃허브 에서 확인 해보실 수 있습니다.

목차

1. useRef 넌 누구니?

이번에도 역시 공식 문서를 참고해봅니다. 공식문서만한게 없죠

즉, useRef를 사용한 값을 변경하면 렌더링에 영향을 주지 않습니다.
그리고 useRef는 두가지 사용 방법이 있습니다.

  1. 값 참조
  2. DOM 조작

값을 참조하는 방식으로 쓸려면 아래와 같은 방식으로 사용해야 합니다.
useRef를 호출해서 안에 initialValue를 넣으면 ref.current로 값의 읽기/쓰기가 가능합니다.

const numberRef = useRef(10);

console.log(numberRef.current); // 10
numberRef.current = 20;
console.log(numberRef.current); // 20

만약 DOM을 조작 해주고 싶다면 initialValue로 null을 넣으면 됩니다.
그러면 ref.current로 해당 돔노드의 접근이 가능해집니다.

const divRef = useRef(null);

return (
  <div ref={divRef} />
);

1-1. 어떻게 구현할까?

useRef 함수는 useState와 비슷하게 생겼을겁니다.
하지만 그 안에서 리렌더링을 일으키는 함수는 실행되지 않죠

useRef함수는 initialValue가 null이면 배열에 넣어주기만 하고
커밋 페이즈(참고)에서 key값중 ref가 있다면 해당 ref안에 현재 DOM Node를 넣어주면 될겁니다.

2. 구현해보자

우선 비교적 간단해 보이는 값 참조 부분부터 구현 해보겠습니다.

2-1. 값 참조 구현

정말 간단하게 구현할 수 있으므로 따로 설명을 하진 않겠습니다.

export interface Ref<T> {
  current: T;
}

const refs: any[] = [];
let refIdx = 0;

export function useRef<T>(ref: T | null): Ref<T> {
  if (refs.length === refIdx) {
    refs.push({ current: ref });
  }

  return refs[refIdx++];
}

export function resetRefIdx() {
  refIdx = 0;
}

이 함수 역시 idx 기반으로 작동하므로 render함수가 실행되기 전 idx를 초기화 해주도록 하겠습니다.

export function render() {
  resetStateIdx();
  resetEffectIdx();
  resetRefIdx(); // << 추가!
  ...
}

제대로 작동하는지 한번 확인해 보도록 하겠습니다.

// App.tsx
function App() {
  const counterRef = useRef(10);

  const incrementCounter = () => {
    counterRef.current = counterRef.current + 1;
    console.log(counterRef.current);
  };

  return (
    <div>
      <button onclick={incrementCounter}>
        카운터 토글 {counterRef.current}
      </button>
    </div>
  );
}


원하던대로 리렌더링은 되지 않고 값만 증가하고 있습니다.

2-2. DOM 조작 구현

리액트의 렌더 페이즈가 끝난 후 커밋 페이즈도 끝나면 ref에 돔노드가 들어가게 됩니다. 저희가 만든 함수중 createDOM함수가 돔을 그려주는데 이 함수를 조금 수정하면 될것같습니다.

export function createDOM(element: VirtualDom): RealDom | Text {
  ...
  if (element.props) {
    Object.keys(element.props).forEach((key) => {
      // key에 ref가 존재할 경우 ref.current에 실제 dom을 할당한다.
      if (key === "ref") {
        element.props[key].current = dom;
      }
      ...
    });
  }
}

정말 간단한 코드인데 실제로 리액트에서는 이렇게 간단하게 동작하지는 않고 비슷하게 동작한다는걸 생각해볼 수 있습니다. 실제 리액트 코드에서는 어떻게 다뤄지는지에 대해서는 다음 기회에 다뤄보도록 하습니다.

그럼 진짜 동작하는지 확인할 차례입니다.

// App.tsx
function App() {
  const divRef = useRef<HTMLDivElement>(null);

  const changeColor = () => {
    divRef.current.style.backgroundColor = "blue";
  };

  return (
    <div ref={divRef} style="padding: 10px; background-color: red;">
      <button onclick={changeColor}>색상을 바꾸는 버튼!</button>
    </div>
  );
}


예상대로 잘 작동합니다!🎉🎉🎉🎉🎉🎉🎉

마무리

이렇게 useState, useEffect, useRef와 함께 가상돔을 간단하게 구현 해 보았습니다.

계기는 네이버 부스트 캠프 과정에서 미션이었습니다. 구현을 해야하는데 제한사항이 바닐라JS만으로 구현을 하는거라 너무 귀찮았던것 같아요. 그래서 리액트를 따라만들자! 해서 그때 과정을 글로 옮겨보았습니다.

참조

https://ko.react.dev/learn/render-and-commit#step-2-react-renders-your-components

profile
기록하는 블로그

0개의 댓글