ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • useRef() 훅과 컴포넌트 외부의 전역 변수 비교: useRef() vs 변수 (let, const)
    React 2024. 3. 9. 15:10

    서론

    useRef를 사용하면서 느끼는 궁금점 중에 useRef의 current 프로퍼티를 변경하더라도 이것이 리렌더링을 발생하지 않는다고 알고 있었습니다. 따라서 값이 변경되더라도 render를 발생시키지 않습니다. 즉 state가 변화됐을 때처럼 리렌더링이 발생하게 된다면 변화된 ref객체의 current프로퍼티값이 화면에 드러납니다. 여기서 발생한 의문이 컴포넌트 외부에 let, const 등을 선언한 global variable도 똑같은 현상이 발생하는 것이 아닌가? 라는 의문이 들었습니다. 따라서 useRef를 사용했을 때와 전역 변수를 사용했을 때의 차이점을 파악하기 위해 포스팅을 하게 되었습니다. 

     


     

    useRef란?

    글을 시작하기에 앞서 먼저 간단하게 useRef에 대해서 알아 봅시다. 

     

    useRef란 React에서 제공되는 훅 중 하나로 .current 프로퍼티로 전달된 인자로 초기화된 변경 가능한 ref 객체를 반환합니다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지됩니다. 

    useRef는 크게 두 가지 주요 용도로 사용됩니다.

    1. DOM 요소에 접근  2. 변수를 관리

     

    1. DOM 요소에 접근

    javaScript를 사용할 때는 특정 DOM요소를 선택하기 위해서 document객체의 querySelector, getElementById 메서드 등을 사용했습니다. React에서도 특정 DOM요소에 접근해야 하는 상황이 생기는데, 이때 ref를 사용합니다.

    함수형 컴포넌트에서 useRef 훅을 사용하여 ref를 사용하는데, 이 포스팅에서는 함수형 컴포넌트를 기준으로 작성하겠습니다. 

     

    아래 간단한 예제를 같이 살펴보겠습니다. 

    import React, { useRef } from 'react';
    
    function FocusInputOnButtonClick() {
      const inputRef = useRef(null);
    
      const handleButtonClick = () => {
        if (inputRef.current) {
          inputRef.current.focus();
        }
      };
    
      return (
        <div>
          <input ref={inputRef} type="text" />
          <button onClick={handleButtonClick}>Focus Input</button>
        </div>
      );
    }
    
    export default FocusInputOnButtonClick;
    • 우선 useRef() 훅을 사용하여 inputRef 변수를 선언합니다. 
    • <input> 요소는 ref={inputRef} 속성을 통해 inputRef 변수와 연결됩니다.
    • inputRef.current는 해당 <input> 요소를 참조하게 됩니다.
    • 버튼 클릭 후 handleButtonClick 메서드가 호출되면 inputRef.current (input DOM요소)의 focus() 메서드가 실행되어 실행되어 입력란에 초점이 맞추어집니다.

    위 코드 예제에서는 useRef() 훅을 사용하여 DOM 요소에 접근하고, 사용자의 클릭 이벤트가 발생했을 때 해당 DOM 요소의 특정 메서드인 focus()를 호출하는 방식으로 useRef() 훅을 활용한 DOM 요소 접근 예시를 살펴보았습니다.

     

     

    2. 변수를 관리 

    앞서 말씀드린것처럼 useRef()를 사용한 객체는 .current 프로퍼티가 변경되어도 리렌더링 되지 않습니다. 따라서 변경 시 렌더링을 발생시키지 말아야 할 변수를 다룰 때 정말 유용합니다. 


    리액트 컴포넌트에서의 useState()로 선언된 상태는 상태를 바꾸는 함수를 호출하고 나서 그다음 렌더링 이후로 업데이트된 상태를 조회할 수 있지만, useRef로 관리하고 있는 변수는 설정 후 바로 조회할 수 있습니다. 

    useRef()로 생성한 객체는 일반적으로 참조 변수를 저장하는 용도로 사용되며 .current 프로퍼티에 값을 할당하여 저장할 수 있습니다. 그러나 .current 프로퍼티에 새로운 값을 할당하는 것은 리액트가 컴포넌트를 다시 렌더링 할 때만 감지할 수 있습니다.

    따라서, useRef()로 관리되는 변수를 설정 후 바로 조회하는 것은 가능하지만, 해당 변수가 변경된 이후에는 컴포넌트의 리렌더링을 통해 변경된 값을 확인해야 합니다.

     

    따라서 다음과 같은 변수를 다룰 때 유용합니다. 

    • setTimeout, setInterval로 만들어진 id
    • scroll의 위치
    import React, { useRef } from 'react';
    import UserList from './UserList';
    
    function App() {
      const users = [
        {
          id: 1,
          username: 'subin',
          email: 'a@example.com'
        },
        {
          id: 2,
          username: 'user1',
          email: 'b@example.com'
        },
        {
          id: 3,
          username: 'user2',
          email: 'c@example.com'
        }
      ];
    
      const nextId = useRef(4);
      const onCreate = () => {
      
        // 배열에 새로운 항복 추가하는 로직 생략
        
        nextId.current += 1;
      };
      return <UserList users={users} />;
    }
    
    export default App;
    • useRef() 훅을 사용하여 새로운 사용자를 추가할 때 다음 id값을 관리하기 위한 nextId변수를 생성합니다. 
    • nextId 변수는 useRef(4)로 초기값이 설정되어 있습니다. 이는 현재 가장 큰 ID 값에 1을 더한 값을 가지게 됩니다.
    • onCreate 함수는 새로운 사용자를 추가하는 로직이 있는데, 이때 nextId.current 값을 사용하여 새로운 사용자의 ID를 설정하고, nextId.current에 1을 더하여 다음 사용자의 ID를 준비합니다. 따라서 사용자가 추가될 때마다 고유한 ID를 가집니다. 

    위의 예제에서 useRef()를 사용하는 이유는 nextId 변수를 컴포넌트 외부에서 관리하면서도, 리렌더링 되어도 유지되어야 하는 값을 저장하기 위해서입니다. nextId 변수를 useState()로 선언한 상태변수 또는 컴포넌트 내부 let변수로 선언하면 컴포넌트가 리렌더링 될 때마다 초기화되어 이전 ID 값을 유지할 수 없습니다. 따라서 useRef()를 사용하여 nextId 변수를 생성하고 새로운 사용자의 ID를 관리하고 유지할 수 있습니다. 

     

     

    컴포넌트의 렌더링에 따른 ref객체의 동작

    컴포넌트가 useState()의 상태가 변하거나 props가 변하는 등의 렌더링이 발생하면, useRef()를 사용하여 생성한 ref객체의 참조값은 동일하고 그때의 current 프로퍼티 값은 업데이트되어 화면에 보일 수 있습니다. 

     

    예를 들어

    import React, { useState, useRef } from 'react';
    
    function Counter() {
      // useState를 사용하여 상태를 관리
      const [count, setCount] = useState(0);
    
      // useRef를 사용하여 ref 객체를 생성
      const countRef = useRef(0);
    
      // 버튼 클릭 시 count 상태 업데이트
      const handleIncrement = () => {
        setCount(prevCount => prevCount + 1);
      };
    
      // 버튼 클릭 시 countRef의 current 값 업데이트
      const handleIncrementWithRef = () => {
        countRef.current += 1;
        // 컴포넌트 리렌더링을 유발하지 않음
      };
    
      return (
        <div>
          <h2>Counter with useState:</h2>
          <p>Count (useState): {count}</p>
          <button onClick={handleIncrement}>Increment</button>
    
          <h2>Counter with useRef:</h2>
          <p>Count (useRef): {countRef.current}</p>
          <button onClick={handleIncrementWithRef}>Increment</button>
        </div>
      );
    }
    
    export default Counter;
    • handleIncrementWithRef 함수에서 countRef.current 값을 업데이트해도, 해당 변경은 즉시 화면에 반영되지 않습니다. useRef는 컴포넌트를 리렌더링 하지 않으므로, current 값의 변경이 리렌더링을 유발하지 않습니다.
    • 화면에 반영되지 않는 이유는 React의 렌더링과 업데이트 사이클에 기인합니다. React는 상태의 변화가 있을 때에만 리렌더링을 수행하며, useRef의 current 값이 변경되더라도 컴포넌트의 상태가 변경되지 않으면 리렌더링이 발생하지 않습니다.
    • 따라서 handleIncrementWithRef 함수에서 countRef.current 값을 업데이트하더라도, 해당 변경이 화면에 반영되기 위해서는 컴포넌트가 리렌더링 되어야 합니다. 만약 handleIncrementWithRef 함수를 호출할 때 setCount를 사용하여 상태를 변경하면, 이로 인해 리렌더링이 발생하고 countRef.current의 변경도 함께 화면에 반영됩니다.

     


     

    useRef()로 선언한 객체 vs 컴포넌트 외부의 전역변수 

     

    위에서 useRef에 대해 알아봤습니다. 이제 서론에서 말씀드렸던 useRef() 훅으로 사용한 객체와 컴포넌트 외부의 전역변수의 차이점에 대해서 파악해 보도록 하겠습니다. 
    여기서 전역변수(global variable)는 컴포넌트 외부에서 선언된 let변수를 의미합니다.

     

     

    • 앞서 말한 것처럼 useRef를 사용하면 current 프로퍼티가 변경되어도 리렌더링이 발생하지 않습니다. state나 props가 바뀌는 등 컴포넌트가 리렌더링 되는 일이 발생하면 변경된 ref객체를 화면에서 볼 수 있습니다.
    • 컴포넌트 밖의 전역 변수(let)도 내부에 선언한 변수와 달리 리렌더링마다 초기화되지 않고, 컴포넌트 내부에서 값을 변경할 수 있다. 이러면 도대체 무슨 차이인가? useRef대신에 컴포넌트 외부의 전역변수를 사용해도 문제없는 거 아닌가?

     

    결론부터 말씀드리자면, 해당 컴포넌트를 1개만 사용할 때는 가능하다입니다.

    • 전역변수로 선언한 변수는 여러 컴포넌트에서 사용하게 되면 그 값을 모두 공유합니다. 즉 해당 컴포넌트를 사용한 모든 곳에서 그 값을 공유하며, 전역변수값은 항상 유지되며 다른 컴포넌트에서 값을 변경하면 해당 변경사항을 같은 모든 컴포넌트에서 공유합니다.
    • 하지만 useRef로 선언한 객체는 컴포넌트와 생애주기를 함께합니다. 컴포넌트의 생성 지점에서 초기화되고, 컴포넌트가 unMounted 되면 메모리에서 해제됩니다. 즉, 변수가 같은 모든 컴포넌트에서 공유되는 것이 아니라 컴포넌트 사용주기에 맞게 변수도 새롭게 초기화됩니다.

     

    다음 예시를 함께 봅시다.

     

    컴포넌트 외부의 전역변수 사용

    import React, { useState } from "react";
    
    let counterOutside = 0;
    const Counter = () => {
      const [counter, setCounter] = useState(0);
      console.log(counterOutside);
      return (
        <p>
          The counter is {counter}{" "}
          <button
            onClick={() => {
              setCounter(counter + 1);
              counterOutside = counterOutside + 1;
            }}
          >
            +
          </button>
        </p>
      );
    };
    
    export default Counter;
    // ....
    <div>
      <Counter /> // counter1
      <Counter /> // counter2
    </div>

     

    1. 초기 상태:
      • counter1 (첫 번째 Counter 컴포넌트 내부): 0
      • counterOutside (외부): 0
      • counter2 (두 번째 Counter 컴포넌트 내부): 0
    2. 첫 번째 버튼 클릭:
      • counter1 (첫 번째 Counter 컴포넌트 내부): 1 (증가)
      • counterOutside (외부): 1 (증가)
      • counter2 (두 번째 Counter 컴포넌트 내부): 0
    3. 두 번째 버튼 클릭:
      • counter1 (첫 번째 Counter 컴포넌트 내부): 1 (변화 없음)
      • counterOutside (외부): 2 (증가)
      • counter2 (두 번째 Counter 컴포넌트 내부): 1 (증가)

    이처럼 counter 컴포넌트를 2개 사용하게 되면 한 컴포넌트에서의 값 변경이 다른 컴포넌트의 값 변경에 영향을 줍니다.

    • React 컴포넌트에서 useState를 사용하여 상태를 관리하면 컴포넌트가 리렌더링 될 때마다 해당 상태가 초기화됩니다. 그렇기 때문에 Counter 컴포넌트 내의 counter 상태는 각각의 인스턴스마다 독립적으로 관리됩니다.
    • 하지만 counterOutside 변수는 컴포넌트 외부에 선언되어 있기 때문에 모든 Counter 컴포넌트 인스턴스에서 공유됩니다. 따라서 counterOutside는 모든 Counter 컴포넌트에서 동일한 값을 가지게 됩니다.

     

    useRef 사용

    import React, { useState, useRef } from 'react';
    
    const Counter = () => {
      const [counter, setCounter] = useState(0);
      const counterRef = useRef(0);
    
      const handleClick = () => {
        setCounter(counter + 1);
        counterRef.current += 1;
        console.log(counterRef.current);
      };
    
      return (
        <p>
          The counter is {counter} <button onClick={handleClick}>+</button>
        </p>
      );
    };
    
    export default Counter;
    // ....
    <div>
      <Counter /> // counter1
      <Counter /> // counter2
    </div>
    1. 초기 상태:
      • counter1 (첫 번째 Counter 컴포넌트 내부): 0
      • counterRef1: 0 (첫 번째 Counter 외부 참조)
      • counterRef2: 0 (첫 번째 Counter 외부 참조)
      • counter2 (두 번째 Counter 컴포넌트 내부): 0
    2. 첫 번째 버튼 클릭:
      • counter1 (첫 번째 Counter 컴포넌트 내부): 1 (증가)
      • counterRef1: 1 (증가)
      • counterRef2: 0 (변화 없음)
      • counter2 (두 번째 Counter 컴포넌트 내부): 0 (변화 없음)
    3. 두 번째 버튼 클릭:
      • counter1 (첫 번째 Counter 컴포넌트 내부): 1 (변화 없음)
      • counterRef1: 1 (변화 없음)
      • counterRef2: 1 (증가)
      • counter2 (두 번째 Counter 컴포넌트 내부): 1 (증가)

     

    각 컴포넌트의 counterRef는 독립적으로 생성되어 서로에게 영향을 주지 않습니다. counterRef1은 첫 번째 컴포넌트의 버튼 클릭에 의해만 영향을 받으며, counterRef2는 두 번째 컴포넌트의 버튼 클릭에 의해만 영향을 받습니다.

     

     

    즉, 정리를 하자면 컴포넌트를 1개만 사용할 때는 전역변수와 useRef()가 동작하는 방식은 같지만 컴포넌트가 2개 이상 사용되면 전역변수는 모든 컴포넌트 인스턴스에서 공유되고 useRef객체는 컴포넌트 생명주기를 따르기 때문에 결과가 달라집니다.

     

     

     

     

     

    참고

    https://react.vlpt.us/basic/10-useRef.html

    https://markoskon.com/the-difference-between-refs-and-variables/

    https://velog.io/@juno7803/React-useRef-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0

    https://programming119.tistory.com/266

    https://yoonjong-park.tistory.com/entry/React-useRef-%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80

Designed by Tistory.