[React] useRef - 컴포넌트의 변수 생성하기

Ahnzi·2024년 11월 20일

one-bite-react

목록 보기
5/11

useRef

이번 시간에는 useRef 기능에 대해 살펴봅니다.

useRef는 use Reference의 줄임말로, 새로운 Reference 객체를 생성하는 기능을 제공합니다. 생성된 Reference 객체는 컴포넌트 내부에서 변수처럼 사용되며, 일반적인 값을 저장할 수 있습니다.

useRef는 useState와 비슷해 보입니다. 두 기능 모두 컴포넌트 내부에서 사용할 변수를 생성하기 때문입니다.
- useRef : Reference 객체를 생성. 컴포넌트 내부의 변수로 활용 가능
- useState : State를 생성. 컴포넌트 내부의 변수로 활용 가능

그러나 useState는 값이 변경되면 컴포넌트를 리렌더링 시키는 반면, useRef로 생성한 변수는 값이 변경되더라도 컴포넌트를 리렌더링하지 않습니다. 따라서 렌더링에 영향을 미치지 않는 변수를 생성하고자 할 때 useRef를 사용합니다.

또한, useRef를 사용하면 컴포넌트가 렌더링한 특정 DOM 요소에 직접 접근할 수 있으며, 이를 조작하는 것도 가능합니다. 이를 통해 특정 요소에 포커스를 주거나, 스타일을 동적으로 변경하는 등의 동작을 손쉽게 구현할 수 있습니다.


useRef를 사용하기 위해 이전에 실습했던 Register.jsx 파일의 상단에서 useRef를 불러옵니다.

import { useRef } from "react";

새로운 Reference 객체를 생성하고 console.log()를 통해 Reference 객체를 확인합니다.

import { useRef } from "react";

const Register = () => {
  const refObj = useRef();
  console.log(refObj);
}

브라우저 콘솔에 current 객체가 출력됩니다. 이 객체가 바로 Reference 객체입니다. Reference 객체는 current라는 프로퍼티를 통해 값을 저장하는 단순한 자바스크립트 객체입니다.

예를 들어, useRef를 사용해 새로운 Reference 객체를 생성할 때 "0"이라는 초기값을 전달하면, current 프로퍼티에 "0"이라는 초기값을 저장한 Reference 객체가 출력되는 것을 확인할 수 있습니다.

import { useRef } from "react";

const Register = () => {
  const refObj = useRef(0);
  console.log(refObj);
}

Reference 객체의 값을 사용하려면 일반 객체를 다룰 때처럼 . 표기법을 사용하여 refObj.current로 접근하면 됩니다.

import { useRef } from "react";

const Register = () => {
  const refObj = useRef(0);
  console.log(refObj.current);
}

Reference 객체는 State와 달리, 값이 변경되더라도 컴포넌트를 리렌더링하지 않습니다. 따라서 Reference 객체는 렌더링에 영향을 주지 않아야 하는 변수를 컴포넌트 내부에서 생성하고 관리할 때 유용하게 활용됩니다.


렌더링 횟수 Count 하기

Reference 객체를 사용하여 Register 컴포넌트에서 렌더링 중인 네 개의 폼에 대해 사용자가 입력값을 얼마나 자주 변경했는지 수정 횟수를 카운트하는 기능을 구현합니다.

refObj의 변수명을 countRef로 변경합니다. 그런 다음, onChange 함수 안에서 countRef.current++로 값을 1씩 증가시키고, console.log를 사용해 countRef.current의 값을 출력해 확인합니다.

import { useState, useRef } from "react";

const [input, setInput] = useState({
    name: "",
    birth: "",
    country: "",
    bio: "",
  });

  const countRef = useRef(0);

  const onChange = (event) => {
    countRef.current++;
    console.log(countRef.current);
    setInput({
      ...input,
      [event.target.name]: event.target.value,
    });
  };

  return (
    <div>
      <div>
        <input name="name" value={input.name} onChange={onChange} placeholder={"이름"} />
      </div>
      <div>
        <input type="date" name="birth" value={input.birth} onChange={onChange} />
      </div>
      <div>
        <select name="country" value={input.country} onChange={onChange}>
          <option value=""></option>
          <option value="kr">한국</option>
          <option value="us">미국</option>
          <option value="uk">영국</option>
        </select>
      </div>
      <div>
        <textarea name="bio" value={input.bio} onChange={onChange} />
      </div>
    </div>
  );
};

export default Register;

수정할 때마다 브라우저 콘솔에 수정 횟수가 출력되는 것을 확인할 수 있습니다.


회원가입 정보 제출하기

새로운 Reference 객체를 생성해서 Register 컴포넌트가 렌더링하고 있는 DOM 요소를 직접 조작해봅니다.

회원가입 정보를 제출하는 기능을 구현하기 위해 새로운 버튼 태그를 추가합니다. 이 버튼을 클릭했을 때 onClick 속성을 통해 onSubmit 이벤트 핸들러가 실행되도록 설정합니다.

import { useState, useRef } from "react";

const [input, setInput] = useState({
    name: "",
    birth: "",
    country: "",
    bio: "",
  });

  const countRef = useRef(0);

  const onChange = (event) => {
    countRef.current++;
    console.log(countRef.current);
    setInput({
      ...input,
      [event.target.name]: event.target.value,
    });
  };

  const onSubmit = () => {};

  return (
    <div>
      <div>
        <input name="name" value={input.name} onChange={onChange} placeholder={"이름"} />
      </div>
      <div>
        <input type="date" name="birth" value={input.birth} onChange={onChange} />
      </div>
      <div>
        <select name="country" value={input.country} onChange={onChange}>
          <option value=""></option>
          <option value="kr">한국</option>
          <option value="us">미국</option>
          <option value="uk">영국</option>
        </select>
      </div>
      <div>
        <textarea name="bio" value={input.bio} onChange={onChange} />
      </div>

      <button onClick={onSubmit}></button>
    </div>
  );
};

export default Register;

onSubmit 이벤트 핸들러에서는 먼저 사용자가 이름 입력란에 올바른 값을 입력했는지 확인합니다. 만약 입력값이 빈 문자열이라면, 이름 입력란에 focus를 설정합니다. 이름 입력란에 focus 기능을 추가하려면 해당 input 태그에 접근할 수 있어야 하며, 이를 위해 Reference 객체를 사용합니다. 새로운 inputRef를 생성하고, 이를 이름 입력란의 ref 속성에 연결합니다.

import { useState, useRef } from "react";

const [input, setInput] = useState({
    name: "",
    birth: "",
    country: "",
    bio: "",
  });

  const countRef = useRef(0);
  const inputRef = useRef();

  const onChange = (event) => {
    countRef.current++;
    console.log(countRef.current);
    setInput({
      ...input,
      [event.target.name]: event.target.value,
    });
  };

  const onSubmit = () => {};

  return (
    <div>
      <div>
        <input
          name="name"
          value={input.name}
          onChange={onChange}
          placeholder={"이름"}
          ref={inputRef}
        />
      </div>
      <div>
        <input type="date" name="birth" value={input.birth} onChange={onChange} />
      </div>
      <div>
        <select name="country" value={input.country} onChange={onChange}>
          <option value=""></option>
          <option value="kr">한국</option>
          <option value="us">미국</option>
          <option value="uk">영국</option>
        </select>
      </div>
      <div>
        <textarea name="bio" value={input.bio} onChange={onChange} />
      </div>

      <button onClick={onSubmit}></button>
    </div>
  );
};

export default Register;

이제 input 태그가 렌더링하는 DOM 요소가 inputRef라는 Reference 객체의 current 속성에 저장됩니다. onSubmit 이벤트 핸들러에서 inputRef.current 값을 console.log로 출력하도록 코드를 작성한 후, 회원가입 버튼을 누르면 브라우저 콘솔에 input DOM 요소가 정상적으로 출력되는 것을 확인할 수 있습니다.

이제 inputRef에 current라는 값에 현재 우리가 접근하고자 하는 DOM 요소가 저장되어있다는 것을 알았으니 바로 Focus 메소드를 호출해주면 됩니다.

import { useState, useRef } from "react";

const [input, setInput] = useState({
    name: "",
    birth: "",
    country: "",
    bio: "",
  });

  const countRef = useRef(0);
  const inputRef = useRef();

  const onChange = (event) => {
    countRef.current++;
    console.log(countRef.current);
    setInput({
      ...input,
      [event.target.name]: event.target.value,
    });
  };

  const onSubmit = () => {
    if (input.name === "") {
      // 이름을 입력하는 DOM 요소 포커스
      inputRef.current.focus();
    }
  };

  return (
    <div>
      <div>
        <input
          name="name"
          value={input.name}
          onChange={onChange}
          placeholder={"이름"}
          ref={inputRef}
        />
      </div>
      <div>
        <input type="date" name="birth" value={input.birth} onChange={onChange} />
      </div>
      <div>
        <select name="country" value={input.country} onChange={onChange}>
          <option value=""></option>
          <option value="kr">한국</option>
          <option value="us">미국</option>
          <option value="uk">영국</option>
        </select>
      </div>
      <div>
        <textarea name="bio" value={input.bio} onChange={onChange} />
      </div>

      <button onClick={onSubmit}>회원가입</button>
    </div>
  );
};

export default Register;

useRef를 굳이 사용해야 할까?

컴포넌트 내부에서 리렌더링을 유발하지 않는 countRef 같은 변수를 만들고 싶다면, useRef를 사용하지 않고 단순히 let count = 0;처럼 일반적인 JavaScript 변수를 선언해도 가능합니다.

일반 Javascript 변수 사용

JavaScript 변수를 useRef로 만든 변수 대신 사용해 봅니다. onChange 이벤트 핸들러에서 countRef.current 값을 증가시키는 대신, JavaScript 변수 count를 선언하고, ++ 연산자를 사용해 값을 증가시켜 봅니다.

import { useState, useRef } from "react";

const [input, setInput] = useState({
    name: "",
    birth: "",
    country: "",
    bio: "",
  });

  const countRef = useRef(0);
  const inputRef = useRef();

  let count = 0;

  const onChange = (event) => {
    // countRef.current++;
    // console.log(countRef.current);
    count++;
    console.log(count);
    setInput({
      ...input,
      [event.target.name]: event.target.value,
    });
  };

  const onSubmit = () => {
    if (input.name === "") {
      // 이름을 입력하는 DOM 요소 포커스
      inputRef.current.focus();
    }
  };

  return (
    <div>
      <div>
        <input
          name="name"
          value={input.name}
          onChange={onChange}
          placeholder={"이름"}
          ref={inputRef}
        />
      </div>
      <div>
        <input type="date" name="birth" value={input.birth} onChange={onChange} />
      </div>
      <div>
        <select name="country" value={input.country} onChange={onChange}>
          <option value=""></option>
          <option value="kr">한국</option>
          <option value="us">미국</option>
          <option value="uk">영국</option>
        </select>
      </div>
      <div>
        <textarea name="bio" value={input.bio} onChange={onChange} />
      </div>

      <button onClick={onSubmit}>회원가입</button>
    </div>
  );
};

export default Register;

input 태그를 여러 번 수정하더라도 브라우저 콘솔에는 항상 1이라는 값만 출력됩니다.

즉, count 변수의 값이 1로 고정되어 있다는 것입니다.

사용자가 input 태그에 값을 입력하면 어떤 일이 일어날까요? 코드에서 onChange 이벤트 핸들러가 실행되면서 input 상태가 변경됩니다. 상태가 변경되면 React는 Register 컴포넌트를 리렌더링합니다. 컴포넌트가 리렌더링된다는 것은, 결국 Register 함수가 다시 호출된다는 의미이며, 함수 내부의 모든 코드가 다시 실행됩니다.

이 과정에서 let count = 0; 코드도 매번 실행되어, count 변수가 리렌더링될 때마다 0으로 초기화됩니다.

따라서, 브라우저에서 input 내용을 아무리 많이 수정해도 count 변수는 매번 0으로 초기화되기 때문에, 콘솔에는 항상 1이 출력됩니다.

useRef()useState()를 이용해서 만든 React의 특수한 변수들은 컴포넌트가 리렌더링 된다고 해도 다시 초기화가 되지 않습니다. 왜냐하면 애초에 내부적으로 그렇게 설계가 되어있기 때문입니다.

그렇기 때문에 컴포넌트 내부에 변수가 필요하다면 useRef(), 렌더링에 영향을 주고 싶다면 useState()로 만들어야 합니다.

count 변수를 컴포넌트 밖의 전역 변수로 설정해준다면?

count 변수를 컴포넌트 외부에 선언하면, 수정 횟수 카운트가 useRef 변수를 사용했을 때와 유사하게 동작합니다. 컴포넌트가 리렌더링되더라도 count 값이 초기화되지 않고 유지되기 때문입니다.

문제가 해결된걸까?

그렇지 않습니다. 변수를 컴포넌트 외부에 선언하면 Register 컴포넌트를 한 번만 렌더링하는 상황에서는 문제가 없어 보일 수 있습니다. 그러나 App.jsx에서 Register 컴포넌트를 두 번 이상 렌더링하면 심각한 문제가 발생할 수 있습니다. 이는 외부에 선언된 변수를 여러 컴포넌트가 공유하기 때문입니다. 컴포넌트 간 상태가 독립적으로 유지되지 않고, 한 컴포넌트의 변경이 다른 컴포넌트에 영향을 미치는 상황이 발생할 수 있습니다.

이런 일이 왜 발생하는 것일까요?

원리는 간단합니다. App 컴포넌트에서 Register 컴포넌트를 두 번 렌더링하는 코드를 작성합니다.

import Register from "./components/Register";

function App() {
  return (
    <>
      <Register />
      <Register />
    </>
  );
}

export default App;

App 컴포넌트에서 Register 컴포넌트를 두 번 렌더링한다는 것은, Register.jsx 파일 전체가 두 번 실행되는 것이 아니라, Register 컴포넌트가 두 번 호출되는 것을 의미합니다.

따라서, Register.jsx 파일 외부에 선언된 count 변수는 두 번 생성되지 않고, 한 번만 선언됩니다. 결과적으로, 두 개의 Register 컴포넌트가 동일한 count 변수를 공유하게 되며, 독립적인 상태를 유지하지 못합니다.


React에서는 정말 특별한 경우가 아니라면 컴포넌트 외부에 변수를 선언하는 것을 권장하지 않습니다. 이런 변수를 사용하고 싶으면 useRef()를 이용합니다.

출처

강의명 : [2024] 한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
profile
운동하는 개발자 Ahnzi 입니다.

0개의 댓글