Frontend/React

React Study - 5. ref: DOM에 이름 달기

rachel_13 2023. 4. 16. 01:59

html에서 특정 DOM 요소에 id 값을 부여하는 것처럼 리액트에서도 특정 DOM 요소에 이름을 부여할 수 있는 기능이 있다.

바로 ref(reference의 줄임말)이다.

 

🧐 리액트에서 id을 사용하면 안될까?

JSX안에서 DOM의 id를 달게 될 경우, 컴포넌트가 렌더링될 때마다 이 id값을 전달하게 되는데, 만약 이 컴포넌트를 여러 곳에서 사용하게 되는 경우 Id 값이 중복으로 사용하게 될 경우가 발생한다. id는 고유한 값으로 만들어져야 하는데, 이를 위반하게 되므로, 리액트 팀은 id 사용을 권장하지 않는다.

대신 ref는 컴포넌트 내부에서만 작동하므로 이러한 걱정을 하지 않아도 된다.

 

5.1 ref는 어떤 상황에서 사용해야 할까?

특정 DOM에 작업할 때 ref를 사용한다. 그래서 그 특정 작업이 대체 무엇일까..?

다음과 같은 상황에서 잘 고려해서 쓰면 된다.

 

"컴포넌트가 특정 정보에 대해 기억하고 있으면 좋겠지만, 다시 렌더링되기는 바라지 않을 때 써라!" -by 공식문서

"DOM을 직접적으로 건드려야 할 때 사용하라" -by 리액트를 다루는 기술 143p

 

공식문서의 내용을 조금 더 가져와보면,

ref는 ref.current 속성을 통해 현재 value에 접근할 수 있다. 이 값은 읽기, 쓰기가 모두 가능한데 리액트가 추적하지 않는 구성요소라고 할 수 있다. (앞서 다시 렌더링되지 않는다고 했던것과 같은 맥락이다.) 이것이 React의 단방향 데이터 흐름에서 "탈출 hatch"가 되는 이유이다.

 

ref는 state와 달리 순수한 자바스크립트 객체로서 current속성을 통해 읽고, 수정할 수 있다.

컴포넌트가 재 렌더링될때 ref는 그 값이 유지되는데, state가 다시 세팅되면 컴포넌트가 다시 렌더링 되는 반면, ref는 그 값이 변경이 되어도 다시 렌더링 되지 않는다는 차이가 있다.

 

5.2 ref 사용

예제를 통해 더 자세히 알아보자.

타이머를 하나 만들어 본다고 생각해보자. start, stop 버튼을 클릭하여 타이머를 제어할 수 있다.

 

사용자가 start 버튼을 클릭하고 난 후 얼마만큼의 시간이 지나있는지를 실시간으로 보여줘야 하므로, currentTime(아래 예시에서는 now)을 추적해야 한다. 이 정보는 렌더링(Rendering)에 사용된다. 그래서 state값으로 선언해줘야 한다는 것을 알 수 있다.

 

요구사항 : 사용자가 start버튼을 누르면 1초에 한번씩 시간을 업데이트 해준다.

import React, { useState } from "react";

const StopWatch = () => {
  const [startTime, setStartTime] = useState<number | null>(null); //시작 시간
  const [now, setNow] = useState<number | null>(null); //현재 시간

  const startTimer = () => {
    //타이머 시작
    setStartTime(Date.now()); //현재 시간을 시작 시간으로 설정
    setNow(Date.now()); //현재 시간을 현재 시간으로 설정

    setInterval(() => {
      //1초마다 현재 시간을 갱신
      setNow(Date.now());
    }, 10);
  };

  let secondPassed = 0;
  if (startTime != null && now != null) {
    //타이머가 시작되었을 때
    secondPassed = (now - startTime) / 1000; //초단위로 변환
  }

  return (
    <div>
      <h1>
        시간 : <span>{secondPassed.toFixed(3)}</span>
      </h1>
      <button onClick={startTimer}>타이머 시작</button>
    </div>
  );
};

export default StopWatch;

stop 버튼을 눌러서 시간을 멈추게 할 때 현재 존재하는 interval을 취소하고 now 라는 state 값을 업데이트 하는 것을 멈춰야 한다. clearInterval로 구현할 수 있겠지만, 이전에 실행중인 바로 그 interval을 종료하기 위해서 우리는 intervalID에 대한 정보를 알고 있어야 한다. 그런데 이 interval ID값은 렌더링에 이용되지 않는다. 

👉 이런 경우에 ref를 사용하는 것이다

 

  const startTimer = () => {
    //타이머 시작
    setStartTime(Date.now()); //현재 시간을 시작 시간으로 설정
    setNow(Date.now()); //현재 시간을 현재 시간으로 설정

    stopTimer(); //타이머가 이미 실행 중이라면 멈춤
    intervalRef.current = setInterval(() => {
      //1초마다 현재 시간을 갱신
      setNow(Date.now());
    }, 10);
  };

const stopTimer = () => {
    clearInterval(intervalRef.current); //타이머 멈춤
  };
  
<button onClick={startTimer}>타이머 시작</button>
<button onClick={stopTimer}>타이머 멈춤</button>

 

5.3 Ref vs State

refs state
사용: useRef(initialValue)
반환: {currnet: initialValue} 
사용: useState(initialValue)
반환: [value, setValue] 
재 렌더링을 트리거하지 않는다. 상태가 바뀌면 재 렌더링이 트리거 된다.
렌더링 프로세스에 구애받지 않고
current의 value를 수정할 수 있다. - Mutable (변하는)
상태를 변경할 때 setter 함수를 호출하여
수정할 수 있으며, setter 함수(setState)가 호출되면
자동으로 렌더링 대기열에 올라간다.
- Immutable (불변의)
렌더링이 되고 있는 동안 current value를 읽을 수 없다. 아무때나 state값을 읽을 수 있다.
그러나, 각 렌더링 마다 state가 snapshot처럼 찍혀있어서 변하지 않는다.

 

5.4 대표적인 사용처

컴포넌트가 외부에서 참조되어야 할 때 사용하다. 일반적으로는 외부 API와 통신할 때 (예를 들면 브라우저 API) 가장 많이 사용한다.

그 대표적인 예는 다음과 같다.

1. timeout 또는 interval ID 값을 저장해야 할 때

2. DOM 요소를 수정, 저장해야 할 때

3. JSX 요소를 계산하지 않아도 되는 다른 객체를 저장할 때

 

5.5 Recap

 

1) Ref는 렌더링에 사용되지 않는 값을 유지하기 위한 이스케이프 해치이다. (자주 필요하지 않을 것으로 예상된다.)

2) Ref는  current라는 단일 속성을 가진 일반 JavaScript 객체로 수정이 가능하다.

3) 리액트에서는 useRef hook을 호출하여 ref를 선언할 수 있다.

4) state와 같이 Ref도 컴포넌트 간의 리 렌더에 값이 유지된다.

5) state와 달리 Ref는 current의 value가 변경되었다고  해서, 그것이 리렌더를 트리거하지 않는다.

6) 렌더링 중에는 ref.current를 사용하지 말라. 이것은 컴포넌트를 예측하기 어렵게 만든다.

 

 

 

 

 

 

ㅡ 본 글은 리액트를 다루는 기술 도서와 리액트 공식 문서를 참고하여 작성한 글입니다. ㅡ