7 분 소요

1. ref와 useRef()

함수형 컴포넌트에서 useRef()를 호출하면 ref 오브젝트를 반환해준다.

const ref = useRef(value);

ref 오브젝트는 이렇게 생겼다: { current: value }

인자로 넣어준 초기값은 ref 객체의 current에 저장된다.

ref 객체는 수정이 가능하기 때문에 언제든 원하는 값으로 바꿔줄 수 있다.

반환된 ref는 컴포넌트의 전 생애주기를 통해 유지된다.

즉, 컴포넌트가 계속해서 렌더링되어도, 컴포넌트가 언마운트 되기 전까지는 값을 그대로 유지할 수 있다는 것이다!!




2. useRef()가 유용한 상황

2.1. 저장공간

ref는 state와 비슷하게 어떤 값을 저장해두는 저장공간으로 사용된다.

state에 변화가 있으면 자동으로 다시 렌더링되고, 함수형 컴포넌트는 말 그대로 함수이기 때문에, 함수가 다시 호출되게 되면 컴포넌트 내부의 변수들은 다시 초기화된다.

따라서 원하지 않는 렌더링 때문에 곤란한 경우가 있다.

state 대신 ref에 값을 저장해두면 어떤 장점이 있을까??

ref 안에 있는 값을 아무리 변경해도 컴포넌트는 다시 렌더링되지 않는다.

state 대신 ref를 사용하면 불필요한 렌더를 막을 수 있다.

또한, 컴포넌트가 아무리 렌더링되어도 ref의 값은 변하지 않고 유지된다.

따라서, 변경 시 렌더링을 발생시키지 말아야 하는 값을 다룰 때 유용하다.


2.2. DOM 요소에 접근

ref를 통해 DOM 요소에 접근해서 여러 가지 일을 할 수 있다.

input 요소를 클릭하지 않아도 focus를 주고 싶을 때가 한 가지 예다.

예를 들어, 로그인 화면에서 ID를 넣는 input 요소를 클릭하지 않아도 자동으로 focus가 되어 있으면 사용자가 편리하다.

마치 바닐라자바스크립트의 Document.querySelector()처럼~!!




3. 예제: state와 ref의 차이

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  console.log("렌더링...");

  const increaseCountState = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <div>State: {count}</div>
      <button onClick={increaseCountState}>State 올려</button>
    </div>
  );
};

export default App;

함수형 컴포넌트는 그 자체로 함수이기 때문에 렌더링이 일어날 때마다 함수가 다시 호출되는 것이다. 따라서 위의 코드에서 State 올려 버튼을 누를 때마다 콘솔에는 ‘렌더링…‘이 찍히게 된다.

useRef를 사용해보자!!

import { useState, useRef } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  console.log(countRef); // -> {current: 0}

  console.log("렌더링...");

  const increaseCountState = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <div>State: {count}</div>
      <button onClick={increaseCountState}>State 올려</button>
    </div>
  );
};

export default App;

ref는 하나의 객체이다. 그래서 콘솔에 찍어보면 {current: 0}이 나온다. ref 안에 있는 값에 접근하려면 countRef.current로 접근하면 된다.

import { useState, useRef } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  const increaseCountState = () => {
    setCount(count + 1);
  };

  const increaseRefState = () => {
    countRef.current = countRef.current + 1;
  };

  return (
    <div>
      <div>State: {count}</div>
      <div>Ref: {countRef.current}</div>
      <button onClick={increaseCountState}>State 올려</button>
      <button onClick={increaseRefState}>Ref 올려</button>
    </div>
  );
};

export default App;

Ref 올려 버튼을 눌러도 화면에는 Ref의 값이 변하지 않는 것처럼 보인다. 그 이유는, 내부적으로는 ref의 값이 증가하고 있지만 렌더를 일으키지 않기 때문에 우리가 볼 수 없는 것이다.

Ref 올려 버튼을 마구 누른 다음, State 올려 버튼을 누르게 되면 count state 값의 변경으로 리렌더가 발생하게 되고, 증가한 ref의 값을 볼 수 있게 된다.

콘솔에 ref가 증가할 때마다 찍어줘보자.

import { useState, useRef } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  console.log("렌더링!!!");

  const increaseCountState = () => {
    setCount(count + 1);
  };

  const increaseRefState = () => {
    countRef.current = countRef.current + 1;
    console.log(`Ref: ${countRef.current}`);
  };

  return (
    <div>
      <div>State: {count}</div>
      <div>Ref: {countRef.current}</div>
      <button onClick={increaseCountState}>State 올려</button>
      <button onClick={increaseRefState}>Ref 올려</button>
    </div>
  );
};

export default App;

Ref 올려 버튼을 누를 때마다 ref가 점점 올라가고 있는 걸 확인할 수 있다. 하지만 화면이 렌더링되지 않기 때문에 화면에는 Ref가 0으로 보인다. 이때 State 올려 버튼을 눌러서 렌더를 시켜주면 증가된 ref 값을 확인할 수 있다.




4. 예제: 컴포넌트 내부의 변수와 ref의 차이

import { useRef } from "react";

const App = () => {
  const countRef = useRef(0);
  let countLet = 0;

  const increaseCountRef = () => {
    countRef.current = countRef.current + 1;
    console.log(`Ref: ${countRef.current}`);
  };

  const increaseCountLet = () => {
    countLet = countLet + 1;
    console.log(`Let: ${countLet}`);
  };

  return (
    <div>
      <div>Ref: {countRef.current}</div>
      <div>Let: {countLet}</div>
      <button onClick={increaseCountRef}>Ref 올리기</button>
      <button onClick={increaseCountLet}>Let 올리기</button>
    </div>
  );
};

export default App;

ref와 변수 모두 렌더링를 발생시키지 않기 때문에, 두 버튼을 다 눌러봐도 콘솔에서는 ref와 변수가 모두 증가하는 걸 볼 수 있지만 화면에서는 볼 수 없다.

state를 추가해서 화면을 업데이트 해줘보자!

import { useRef, useState } from "react";

const App = () => {
  const countRef = useRef(0);
  let countLet = 0;
  const [count, setCount] = useState(0);

  const increaseCountRef = () => {
    countRef.current = countRef.current + 1;
    console.log(`Ref: ${countRef.current}`);
  };

  const increaseCountLet = () => {
    countLet = countLet + 1;
    console.log(`Let: ${countLet}`);
  };

  const increaseCountState = () => {
    setCount(count + 1);
    console.log(`렌더됨! count: ${count}`);
  };

  return (
    <div>
      <div>Ref: {countRef.current}</div>
      <div>Let: {countLet}</div>
      <div>State: {count}</div>
      <button onClick={increaseCountRef}>Ref 올리기</button>
      <button onClick={increaseCountLet}>Let 올리기</button>
      <button onClick={increaseCountState}>State 올리기</button>
    </div>
  );
};

export default App;

Ref를 마구 올려주고, Let도 마구 올려준 뒤 State 올리기 버튼을 눌러주면! 새롭게 렌더가 된다. 그러면 화면에 보이지 않던 증가된 Ref가 화면에 보이게 되고, state도 증가한다. 그치만 변수 Let은 여전히 0이다.

콘솔을 확인해보면, 새롭게 렌더되기 전까지는 ref와 let이 잘 올라가고 있는 것을 알 수 있다. state가 변경되어 새롭게 렌더된 후에 둘 사이의 차이를 알 수 있다. ref는 렌더링과 무관하게 이어서 증가하는 반면, let은 다시 초기값 0으로 초기화되어 1, 2, 3… 다시 증가하기 시작한다.

그 이유는 앞서 말했듯, 함수 컴포넌트가 함수이기 때문에 다시 렌더 된다는 것은 함수를 다시 호출한다는 것을 의미하기 때문이다. 함수를 호출하게 되면 함수 내부의 변수들은 초기화된다.

하지만 ref는 아무리 컴포넌트가 렌더링되어도 컴포넌트의 전 생애주기에서 그대로 유지된다. 즉, 해당 컴포넌트가 마운팅 된 시점부터 마운팅 해제되는 시점까지 계속해서 같은 값을 유지한다는 뜻이다.

조금 더 보기 좋게 코드를 바꿔보자.

import { useRef, useState } from "react";

const App = () => {
  const countRef = useRef(0);
  let countLet = 0;
  const [count, setCount] = useState(0);

  const increaseCountRef = () => {
    countRef.current = countRef.current + 1;
    console.log(`Ref: ${countRef.current}`);
  };

  const increaseCountLet = () => {
    countLet = countLet + 1;
    console.log(`Let: ${countLet}`);
  };

  const increaseCountState = () => {
    setCount(count + 1);
    console.log(`렌더됨! count: ${count}`);
  };

  const printResults = () => {
    console.log(`ref: ${countRef.current}, let: ${countLet}`);
  };

  return (
    <div>
      <div>Ref: {countRef.current}</div>
      <div>Let: {countLet}</div>
      <div>State: {count}</div>
      <button onClick={increaseCountRef}>Ref 올리기</button>
      <button onClick={increaseCountLet}>Let 올리기</button>
      <button onClick={increaseCountState}>State 올리기</button>
      <button onClick={printResults}>ref, let  출력</button>
    </div>
  );
};

export default App;

리렌더 전후의 ref와 let의 증가 추이를 볼 수 있게 되었다. ref는 state 값 변경과 무관하게 계속 그 값을 유지하는 반면, let은 새롭게 렌더되면 초기값 0으로 초기화된다.




5. Ref가 매우매우 유용한 상황!! (useEffect 내에서 )

지금까지 총 몇 번이나 렌더됐는지 누적된 값을 알고 싶어!

import { useEffect, useState } from "react";

const App = () => {
  const [count, setCount] = useState(1);
  const [renderCount, setRenderCount] = useState(1);

  useEffect(() => {
    console.log("렌더링!");
    setRenderCount(renderCount + 1);
  });

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>up!!</button>
    </div>
  );
};

export default App;

이렇게 하면 될까?

안 됩니다.

무한루프에 갇히게 됩니다!!

왜 그런 걸까? up!! 버튼을 클릭하게 되면, count가 업데이트 되니까 useEffect가 호출된다. 그런데 useEffect 안에도 renderCount를 업데이트 시키는 코드가 있다. 그럼 또 useEffect가 호출된다. 그럼 또 renderCount가 업데이트 된다. 그럼 또 useEffect가 호출되고….


이때, ref가 빛을 발하게 된다!!

import { useEffect, useRef, useState } from "react";

const App = () => {
  const [count, setCount] = useState(1);
  const countRef = useRef(0);

  useEffect(() => {
    countRef.current = countRef.current + 1;
    console.log(`렌더링 횟수: ${countRef.current}`);
  });

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>up!!</button>
    </div>
  );
};

export default App;

ref는 새로운 렌더를 발생시키지 않으니 무한루프에 빠지지 않는다.

useRef는, 변화는 감지하지만 새로운 렌더를 발생시키면 안 될 때 사용하면 좋다.




6. DOM 요소 접근

ref는 저장공간 말고도 다른 역할을 할 수 있다고 했다.(2.2.) DOM 요소에 직접 접근할 수 있다.

const ref = useRef(value)를 사용하면, { current: value } 라는 ref 객체를 반환한다고 했었다.

이 ref 오브젝트를 우리가 접근하고자 하는 태그에 ref 속성으로 넣어주기만 하면 정말 쉽게 해당 요소에 접근할 수 있다.

<input ref="{ref}" />

대표적인 사용 예시로는 텍스트박스 같은 input 요소에 focus를 줄 때 많이 사용된다.

로그인 페이지에서 로딩되어 화면에 보여질 때 ID를 넣는 공간을 클릭하지 않아도 바로 focus를 주면 마우스를 사용하지 않고 바로 입력할 수 있어서 편리하다.

import { useEffect, useRef } from "react";

const App = () => {
  const inputRef = useRef();

  useEffect(() => {
    console.log(inputRef); // -> Object {current: undefined}
  }, []);

  return (
    <div>
      <input type="text" placeholder="username" />
      <button>로그인</button>
    </div>
  );
};

export default App;

초기값을 설정해주지 않았기 때문에 콘솔을 찍어보면 {current: undefined}가 출력된다.

우리는 이제 저 undefined 자리에 input 요소에 대한 참조를 저장해주면 된다.

input 태그의 ref라는 속성에다가 우리가 만들어준 inputRef를 넣어주면 된다. 객체 구조분해 할당인 것인지?

import { useEffect, useRef } from "react";

const App = () => {
  const inputRef = useRef();

  useEffect(() => {
    console.log(inputRef); // -> {current: input}
    console.log(inputRef.current); // -> <input type="text" placeholder="username">
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="username" />
      <button>로그인</button>
    </div>
  );
};

export default App;

콘솔을 확인해보면 inputRef에는 {current: input}이, inputRef.current에는 input 태그가 출력된다.

우리는 지금 화면이 새로고침 되면 input 요소에 focus를 주고 싶다.

inputRef.current의 값이 input 요소이기 때문에 inputRef.current.focus()와 같이 사용할 수 있게 된다.

import { useEffect, useRef } from "react";

const App = () => {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="username" />
      <button>로그인</button>
    </div>
  );
};

export default App;

마우스로 클릭하지 않아도 페이지가 로드되면 커서가 input 창에서 깜빡거리는 것을 볼 수 있다.

추가로, input 요소에 입력된 값을 alert창으로 띄우고, alert 창이 꺼지면 다시 focus를 주는 작업을 해보자.

import { useEffect, useRef } from "react";

const App = () => {
  const inputRef = useRef();

  useEffect(() => {
    console.log(inputRef.current);
    inputRef.current.focus();
  }, []);

  const login = () => {
    alert(`${inputRef.current.value}님, 환영합니다!`);
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="username" />
      <button onClick={login}>로그인</button>
    </div>
  );
};

export default App;

마찬가지로, inputRef.current의 값으로 input 요소가 할당되어 있기 때문에, inputRef.current.value와 같이 사용할 수 있게 도ㅎ


📌 별코딩 님의 React Hooks에 취한다 - useRef 완벽 정리 1# 변수 관리React Hooks에 취한다 - useRef 완벽 정리 2# DOM 요소 접근을 참고했습니다!

댓글남기기