[React] 다시 한번 useRef를 파헤쳐보자

sohyeon kim·2022년 8월 1일
2

React & Javascript

목록 보기
36/41
post-thumbnail

함수형 컴포넌트에서 useRef를 부르면 ref 오브젝트를 반환해준다.

const ref = useRef(value)
console.log(ref) // {current:value]

인자로 넣어준 초기값 value는 ref 안에 있는 current 에 저장이 된다. ref 오브젝트는 수정이 가능하기 때문에 언제든 원하는 값으로 바꿔줄 수 있다.

const ref = useRef("hi")
console.log(ref) // {current:"hi"]

const ref = useRef("hello")
console.log(ref) // {current:"hello"]

const ref = useRef("nice")
console.log(ref) // {current:"nice"]

반환된 ref 는 컴포넌트의 전생애주기를 통해 유지가 된다. 컴포넌트가 계속해서 렌더링 되어도 즉 컴포넌트가 언마운트 되기 전까지는 값을 그대로 유지할 수 있다.


Q. 그럼 useRef는 언제 사용할까?

대표적으로 2가지 상황이 있다.

1️⃣ 저장공간

스테이트와 비슷하게 어떠한 값을 저장해주는 저장공간으로 사용된다.

state를 변경하면 자동으로 컴포넌트가 다시 렌더링이 된다. 함수형 컴포넌트는 말 그대로 함수다. 그래서 리렌더링이 되면 함수가 다시 불리는 것이기 때문에 내부에 있는 모든 변수들이 다 초기화가 된다. 그렇기 때문에 가끔 원하지 않는 렌더링 때문에 곤란해진다. 이 때 state 대신 Ref 안에 값을 저장해 높으면 되는데 어떤 장점이 있을까?

ref의 장점은 ref 안의 값을 아무리 변경해도 컴포넌트는 다시 렌더링이 되자 않아 불필요한 렌더링을 막을 수 있다. 또한 컴포넌트가 아무리 렌더링이 되어도 ref 안에 있는 값은 변화되지 않고 유지된다. 그렇기 때문에 변경 시 렌더링을 발생시키지 말아야 하는 값을 다룰 때 정말 편리하다.

2️⃣ DOM 요소에 접근

ref를 통해 실재적으로 돔 요소에 접근해 여러 가지 일을 할 수 있다. 대표적으로 input 요소를 클릭하지 않아도 포커스를 주고 싶을 때 사용한다. 로그인 화면에서 아이디 input 창에 자동으로 포커스가 놓이면 바로 아이디를 입력할 수 있어 편리하다. 이는 바닐라 자바스크립트의 Document.querySelector()와 같다.


1️⃣ 저장공간

먼저 저장공간 state와 저장공간 ref의 차이에 대해 알아보자.

count Ref 안에 무엇이 들어있는지 console을 찍어 확인해보자.

import { useState, useEffect } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(0)
  
  console.log(countRef)
  console.log("렌더링 📌");
  
  const increaseCountState = () => {
    setCount(count + 1);
  };

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

export default App;

ref는 하나의 오브젝트고 그 안에는 current 그리고 설정해 준 초기값 0이 있다. 여기서 ref 안에 있는 값에 접근하려면 countRef.current로 접근하면 된다. 그럼 한 번 출력해 보자. 그리고 ref 안에 있는 값을 하나씩 올려주는 버튼도 만들어보자.

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

  console.log(countRef, "🌱");
  console.log("렌더링 📌");

  const increaseCountState = () => {
    setCount(count + 1);
  };
  
  // 📌 추가
  const increaseCountRef = () => {
    countRef.current = countRef.current + 1;
  };

  return (
    <div>
      <p>state:{count}</p>
      <p>Ref:{countRef.current}</p>
      <button onClick={increaseCountState}>state 올려</button>
	 // 📌 추가
      <button onClick={increaseCountRef}>Ref 올려</button>
    </div>
  );
};

export default App;

여기서 state를 올리면 count 가 증가되는 것을 볼 수 있다. 그러나 ref 올려 버튼을 눌렀는데 아무일도 일어나지 않는다.

Q. 왜 그럴까? 정말 아무일도 일어나지 않는 걸까?

답은 아니다.

ref는 아무리 수정을 해도 컴포넌트를 다시 렌더링 시키지 않는다. 컴포넌트 내에서 countRef가 계속해서 증가하는 것은 맞다. 하지만 렌더링이 되지 않기 때문에 아무리 증가해도 화면이 증가하지 않는 것이다.
여기서 다시 스테이트 버튼을 올리면 어떤 일이 일어날까?
다시 렌더링이 되겠지? 그럼 화면이 업데이트되면서 화면의 ref 도 업데이트가 될 것이다.

그럼 조금더 보기 편하게 상단의 console.log(countRef, "🌱") 를 increaseCountRef 함수 안에 넣어보자.

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

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

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

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

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

export default App;

출력을 보면 ref 버튼을 눌렀을때 ref가 점점 올라가는 것을 볼 수 있다.

하지만 화면이 렌더링이 되지 않기 때문에 화면에는 보이지 않는다. 하지만 여기서 state를 눌러주면 전체 화면이 다시 그려지는 것이기 때문에 state도 하나가 올라갔고, Ref 도 보이는 것을 볼 수 있다.

Q. 그럼 도대체 Ref의 장점은 무엇일까?

자주 바뀌는 값을 state 안에 넣어놨다고 가정하면 그 값이 바뀔 때마다 렌더링 되고 또 렌더링 될 것이다. 이러면 성능에 좋지 않다. 이 경우 state가 아니라 ref를 사용하면 그 값이 아무리 바뀌어도 렌더링을 발생시키지 않기 때문에 성능이 좋다.


컴포넌트 내부의 그냥 변수랑 ref의 차이를 알아보자.

import { useState, useRef } from "react";

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

  const increaseRef <= () => {
    countRef.current = countRef.current + 1;
    console.log("Ref:", countRef.current);
  };
  const increaseVar = () => {
    countVar = countVar + 1;
    console.log("var:", countVar);
  };

  return (
    <div>
      <p>Ref:{countRef.current}</p>
      <p>Var:{countVar}</p>
      <button onClick={increaseRef}>Ref 올려</button>
      <button onClick={increaseVar}>Var 올려</button>
    </div>
  );
};

export default App;

ref의 값과 var의 값이 하나씩 증가하는 것을 콘솔을 통해 확인할 수 있다. 여기서 당연히 ref와 var의 변화는 렌더링을 발생시키지 않는다. 그러니 화면은 업데이트가 되지 않는다. 그럼 화면을 업데이트해주는 state를 추가해 보자.

import { useState, useRef } from "react";

const App = () => {
  // 📌 화면출력
  const [renderer, setRenderer] = useState(0);
  const countRef = useRef(0);
  let countVar = 0;
  
   // 📌 화면출력함수
  const doRendering = () => {
    setRenderer(renderer + 1);
  };

  const increaseRef = () => {
    countRef.current = countRef.current + 1;
    console.log("Ref:", countRef.current);
  };
  const increaseVar = () => {
    countVar = countVar + 1;
    console.log("var:", countVar);
  };

  return (
    <div>
      <p>Ref:{countRef.current}</p>
      <p>Var:{countVar}</p>
      <button onClick={doRendering}>렌더!</button>
      <button onClick={increaseRef}>Ref 올려</button>
      <button onClick={increaseVar}>Var 올려</button>
    </div>
  );
};

export default App;


ref를 올리고 var를 올리고 렌더를 누르면 ref는 나왔는데 var는 나오지 않았다.

Q.왜 그럴까?

컴포넌트가 렌더링이 된다는 것은 컴포넌트를 나타내는 함수가 다시 불린다는 것이다. 그때마다 함수 내부에 있는 변수들이 다시 초기화가 된다는 것인데, 즉 countVar 이 변수는 함수가 불릴 때마다 0으로 초기화가 된다는 것이다. 하지만 ref는 아무리 컴포넌트가 렌더링이 되어도 값을 유지한다. 왜냐하면 ref의 값은 컴포넌트의 전 생애 주기를 통해 유지가 되기 때문이다. 즉 컴포넌트가 브라우저 마운팅 된 시점부터 마운트가 해제될 때까지 같은 값을 계속해서 유지할 수 있다는 뜻이다. 그래서 이어서 눌러보면 0부터 시작하지 않고 이전에 가지고 있던 6에서 시작하는 것을 볼 수 있는데 변수 var는 0에서 시작한다.

다시 여기서 렌더를 누르면 ref 업데이트 되었지만 변수는 계속해서 0으로 돌아간다.

여기서 함수 하나를 추가해주자
printResult 함수가 호출되면 현재 ref 안에 있는 값과 변수 안에 있는 값을 출력해준다.


import { useState, useRef } from "react";

const App = () => {
  const [renderer, setRenderer] = useState(0);
  const countRef = useRef(0);
  let countVar = 0;

  const doRendering = () => {
    setRenderer(renderer + 1);
  };

  const increaseRef = () => {
    countRef.current = countRef.current + 1;
    console.log("Ref:", countRef.current);
  };
  const increaseVar = () => {
    countVar = countVar + 1;
    console.log("var:", countVar);
  };

  // 📌 함수 추가
  const printResult = () => {
    console.log(`ref:${countRef.current}, var:${countVar}`);
  };

  return (
    <div>
      <p>Ref:{countRef.current}</p>
      <p>Var:{countVar}</p>
      <button onClick={doRendering}>렌더!</button>
      <button onClick={increaseRef}>Ref 올려</button>
      <button onClick={increaseVar}>Var 올려</button>
		// 📌 버튼 추가
      <button onClick={printResult}>Ref Var 값 출력</button>
    </div>
  );
};
export default App;

ref 3번 올리고, 변수 2번 올리고 이 두 개의 값을 출력하면 ref는 3 변수는 2의 값을 가지고 있다고 출력이 되고, 렌더!를 눌러주면 화면이 업데이트가 되고 다시 한번 값을 출력을 해주면 ref는 렌더링 이후에도 값이 유지가 되지만 변수는 초기화가 된 것을 확인할 수 있다.

자, 그럼 ref 가 굉장히 유용한 상황을 알아보자.

count 가 올라갈때마다 컴포넌트가 몇번 렌더링이 되는지 출력해보자.
간단히 생각해보면 아래처럼 코드를 짤 수 있다.

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

const App = () => {
  const [count, setCount] = useState(1); 
  const [renderCount, setRenderCount] = useState(1);
  
  // 매번 렌더링이 될 때마다 실행 
  // 여기서 setRenderCount를 불러서 renderCount를 하나씩 증가시키면 안될까? no !!!
  // 📌 무한 렌더링
   useEffect(() => {
     console.log('무한렌더링')
     setRenderCount(renderCount+1)
  });
  
  return(
    <div>
      <p>count : {count}</p>
      {/* 올려버튼이 눌릴때마다 count가 1씩 증가 */}
      <button onClick={() => setCount(count + 1)}>올려</button>
    </div>
  );
};

export default App;

무한 루프에 갇힌 것을 확인할 수 있다.

Q. 도대체 왜?

올려 버튼을 클릭하면 count가 업데이트되기 때문에 useEffect가 불릴 것이다. 근데 useEffect 안에도 renderCount state를 업데이트하는 코드가 들어있다. 즉 count state 업데이트했고, useEffect 불렸고, renderCount 도 업데이트되었고, 그럼 또 useEffect 불리고, 또 renderCount 업데이트되고 무한 렌더링

이 때, useRef를 사용한다.
렌더 카운트를 state에 저장하는 것이 아니라 ref를 사용해서 저장할 것이다.

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

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

  
// 📌renderCount 의 값을 하나씩 증가시키는 코드를 넣어라
   useEffect(() => {
	renderCount.current = renderCount.current + 1;
    console.log("렌더링 수:", renderCount.current);
  });
  
  return(
    <div>
      <p>count : {count}</p>
      {/* 올려버튼이 눌릴때마다 count가 1씩 증가 */}
      <button onClick={() => setCount(count + 1)}>올려</button>
    </div>
  );
};

export default App;

자 이제 렌더링 수가 잘 나오는 것을 확인할 수 있다.

Q. 도대체 왜?

ref는 리렌더링을 발생시키지 않으니까 아까 마주친 무한루프에 빠지지 않는다.

결론적으로 useRef는 변화는 감지해야하지만 그 변화가 렌더링을 발생시키면 안되는 어떤 값을 다룰 때 정말 편리하다는 것을 알 수 있다.



2️⃣ DOM 요소에 접근

useRef를 사용해서 DOM 요소에 직접 접근하는 방법을 알아보자.

const ref = useRef(value)
console.log(ref) //{current: value}

<input ref={ref}/>

useRef를 부르면 ref 오브젝트를 반환한다. 이 오브젝트를 접근하고자 하는 요소 태그에 ref 속성으로 바인딩만 해주면 정말 쉽게 해당 요소에 접근할 수 있다.

input 요소에 포커스를 줄 때 많이 사용된다.

예시를 통해 알아보자.
기본적인 로그인 페이지를 하나 만들어 보자.

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

export default App;

input 요소에 자동으로 focus를 주려면 돔 요소에 직접적으로 접근을 할 수 있어야 하는데 useRef를 통해 구현할 수 있다.

  • useRef 의 초기값은 비워준다. 어차피 inputRef 안에는 input 요소가 들어갈 것이기 때문이다.
  • 맨 처음 렌더링이 될 때 실행될 useEffect를 만들어주자. (빈 배열을 넣기)
  • useEffect 안에서 inpufRef에 무엇이 들었는지 확인해 보자.
import { useEffect, useRef } from "react";

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

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

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

export default App;

ref에 초기값은 넣어주지 않았으니까 current 값은 undefined로 나온다.

그리고 이제

<input type="text" placeholder="username" />

이 요소를 undefined 대신에 current 안에 넣어준다. (input 에 대한 참조를 넣어준다)
방법은 간단한다 !

input 태그 안에 ref 라는 속성에 만들어준 inputRef를 넣어준다.

import { useEffect, useRef } from "react";

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

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

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

export default App;

새로고침을 하면 current 안에 input이 들어간 것을 확인할 수 있다.

이제 자동 focus를 잡아보자 !
useEffect 안에서 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;

그럼 자동으로 focus가 잡힌 것을 볼 수 있다.

여기서 더 나아가 username을 입력하고 로그인 버튼을 눌렀을때 텍스트가 팝업 형태로 나오게끔 해보자.

import { useEffect, useRef } from "react";

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

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

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

export default App;


확인 버튼을 누르고 다시 input에 focus가 가도록 해보자.

import { useEffect, useRef } from "react";

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

  useEffect(() => {
     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;

정리하자면 useRef를 사용하면 손쉽게 Dom에 접근할 수 있다. 이 방법은 바로, 포트폴리오 로그인과 회원가입에 적용해야겠다. useRef는 dom에 접근하는 것뿐만 아니라 효율적인 컴포넌트 렌더링과 변수 관리에도 굉장히 효과적인 것을 알 수 있다.



참고
리액트 공식 홈페이지
별코딩-리액트훅스시리즈

profile
slow but sure

0개의 댓글