본문 바로가기

웹 프로그래밍/FrontEnd

[React] useState? useRef? (feat.단축키 추가)

최근에 업무를 진행할 때 리액트로 만들어진 웹사이트에 전역 단축키 이벤트를 추가해야하는 경우가 있었습니다.

 

이때 겪었던 문제 상황과 해결 방법에 대해 간단하게 소개하고 관련 내용을 추가 학습하여 정리해보았습니다.

 

상황을 자세히 설명하자면 리액트로 만들어진 프로젝트에 전역 단축키 기능을 추가해야하고, 로직을 추가할 때 컴포넌트 안의 업데이트 된 state 값을 사용해야하는 상황이었습니다.

 

상황을 간단한 코드로 재현해보면 다음과 같습니다.(코드는 App.js에 중요한 부분만 간단하게 요약하였습니다.)
//App.js
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    //컴포넌트가 렌더링 될 때 전역 단축키 이벤트를 설정한다.
    window.addEventListener("keydown", () => {
      //단축키가 눌렸을때의 로직에서 count 상태를 사용한다고 가정.
      console.log(count);
    });
  }, []);

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

  return <button onClick={onClickBtn}>{count}</button>;
}

버튼이 눌렸을때 count를 업데이트 하고 키가 눌렸을때 count state를 사용해 특정 로직을 실행하는 상황이라고 가정하겠습니다.

 

다음 코드에서 버튼을 몇번 클릭 한뒤 키를 입력하면 콘솔에 업데이트 된 count가 출력될까요?

 

버튼을 11번 클릭했는데도 콘솔에는 0만 출력된다.

아쉽게도 count는 11이 되었지만 콘솔에는 초기값인 0이 출력됨을 알 수 있습니다.

 

 

처음에 이런 현상을 해결하기 위해 useEffect의 의존성 배열에 count 값을 넣은후 return값에 removeEvent를 해줘 이벤트를 붙였다 뗐다 하면서 업데이트 된 state 값을 사용했습니다. 
//App.js
  useEffect(() => {
    //컴포넌트가 렌더링 될 때 전역 단축키 이벤트를 설정한다.

    const shortCut = () => {
      //단축키가 눌렸을때의 로직에서 count 상태를 사용한다고 가정.
      console.log(count);
    };

    window.addEventListener("keydown", shortCut);

    return () => window.removeEventListener("keydown", shortCut);
  }, [count]);

그러면 다음과 같이 콘솔에 업데이트 된 값이 제대로 나오는 것을 알 수 있습니다.

하지만 원래 의도는 컴포넌트가 처음 렌더링 될 때 이벤트를 한번 등록하는거였기도 하고, 계속 이벤트를 붙였다 뗐다 하는 것은 비효율적이고 제약사항도 많아 질 것 같습니다.

 

이럴때는 useState가 아닌 useRef를 사용하는게 좋습니다. 
//App.js

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

  useEffect(() => {
    //컴포넌트가 렌더링 될 때 전역 단축키 이벤트를 설정한다.

    const shortCut = () => {
      console.log(countRef.current);
    };

    window.addEventListener("keydown", shortCut);

    return () => window.removeEventListener("keydown", shortCut);
  }, []);

  const onClick = () => {
    setCount(count + 1);
    countRef.current = count + 1;
  };

  return <button onClick={onClick}>{count}</button>;
}

count는 화면에 숫자를 표시해줘야 하기 때문에 그대로 두고 동일한 값을 countRef를 useRef를 사용해 선언해줍니다.

 

 

 

여기서는 하나의 컴포넌트(App.js)에서 상태 변경과 이벤트 설정을 전부하고 있어 count가 바뀔때마다 이벤트가 삭제 되지만, 하위 컴포넌트가 존재하고 그 컴포넌트에서만 리렌더링이 일어난다고 가정하면 하위 컴포넌트가 리렌더링 되어도 최상위 컴포넌트인 App.js의 이벤트는 삭제되지 않고 업데이트 된 countRef의 값을 사용할 수 있게 됩니다. 

예시코드는 다음과 같습니다.

//App.js

function App() {
  const countRef = useRef(0);

  useEffect(() => {
    console.log("addEvent");

    const shortCut = () => {
      console.log(countRef.current);
    };

    window.addEventListener("keydown", shortCut);

    return () => {
      console.log("removeEvent");
      window.removeEventListener("keydown", shortCut);
    };
  }, []);

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

  return (
    <div>
      <Button />
      <button onClick={onClick}>refUp</button>
    </div>
  );
}
//Button.jsx
function Button() {
  const [count, setCount] = useState(0);
  const clickBtn = () => {
    setCount(count + 1);
  };
  return <button onClick={clickBtn}>{count}</button>;
}

 

다음과 같이 count를 업데이트 시키는 버튼을 하위 컴포넌트로 분리하게 되면 

 

이벤트 등록이 한번만 되는 것을 볼 수 있습니다.(맨 처음 렌더링 될때 add -> remove -> add가 되는 현상은 좀 더 알아봐야 할 것 같습니다.)

 

이렇게 useRef를 사용해 단축키 이벤트 기능을 추가했습니다.

 

그러면 왜 useState는 안되고 useRef는 되는지에 대해 알아보겠습니다.

 

useState vs useRef

 

useState와 useRef는 둘다 어떤 값을 담고 있다는 면에서는 동일하지만, 이 둘의 가장 큰 차이점은 리렌더링입니다.

useState는 해당 값이 바뀌면 컴포넌트가 리렌더링 되지만, useRef는 그 값이 바뀌어도 리렌더링 되지 않습니다.

본질적으로 useRef는 .current 프로퍼티에 변경 가능한 값을 담고 있는 “상자”와 같습니다.
useRef는 내용이 변경될 때 그것을 알려주지는 않는다는 것을 유념하세요. .current 프로퍼티를 변형하는 것이 리렌더링을 발생시키지는 않습니다. 
- react 공식문서 중 useRef -

위 설명처럼 useRef는 변경 가능한 값을 담고 있는 상자와 같습니다. + dom에 접근하는 방법이기도 합니다.

 

즉 reference를 가리키고 있기 때문에 처음 이벤트가 추가되면 ref변수를 가리키게 되고 해당 값이 업데이트 되더라도 reference의 값을 읽어오기 때문에 업데이트 된 값을 그대로 가져올 수 있게 되는 것 같습니다. 

결과적으로 리렌더링이 발생해야할때는 useState, 리렌더링이 발생하지 않아도 될때는 useRef를 사용하는 것이 좋을 것 같습니다.

 

이외에도 setInterval을 사용해 몇초마다 업데이트 된 값을 가지고 특정 로직을 반복해야 하는 상황등에 초기값을 읽어와 의도대로 동작하지 않는 useState대신 useRef를 사용해 의도된 동작을 구현할 수 있습니다.

 

이상으로 단축키 기능을 구현할때의 문제와 해결, useState와 useRef에 대해 알아봤습니다.

 

끝!

반응형
SMALL