개발새발 로그

React에 useId와 useTransition, useDeferredValue 있는 거 아니? 본문

React

React에 useId와 useTransition, useDeferredValue 있는 거 아니?

이즈흐 2024. 4. 9. 21:22

React에서 제공되는 hook들이 있는데 이를 최대한 활용하는 것이 좋다고 개인적으로 생각한다.

특히 생소한 훅들이 몇몇 있는데 그 중에서 3가지를 가져와봤다.

 

React 18에 도입된 새로운 기능인 useId와 useTransition,useDeferredValue 에 대해서 알아보자

 

 

📖useId

https://react.dev/reference/react/useId

 

useId – React

The library for web and native user interfaces

react.dev

 

useId는 아래와 같이 사용한다고 한다.

const id = useId()

 

useId는 어떤 아이디를 생성하는 것으로 보인다.

그럼 useId를 실제로 console.log에 찍으면 어떻게 나올까?

const id = useId();
console.log(id);

 

콜론 사이에 어떤 문자가 나오는데 이처럼 useId는 고유 ID 문자열을 반환한다.

무슨 역할을 하는 것일까?

아래 예제를 보자

function Page() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Name:</label>
      <input id={id} type="text" />
    </>
  );
}

보통 label과 input을 연결하기 위해서는 input의 id 속성을 지정하고, label의 htmlFor 속성을 지정해서 같은 Id값을 줘야한다.

예제를 보면 input태그와 label 태그를 이어주기 위해 idhtmlFor 속성을 사용하고 useId를 사용해서 같은 id를 넣어줬다.

 

이것이 useId의 보편적인 사용방법이라고 한다.

 

위 예제를 응용하면 아래와 같이 사용할 수 있다.

export default function MainPage() {
  return (
    <section>
      <InputComponent />
    </section>
  )
}

function InputComponent() {
  const id = useId()
  return (
    <div>
      <label htmlFor={id}>이름</label>
      <input id={id}></input>
    </div>
  )
}

이 예제를 DOM Element로 보면 아래와 같다.

각각 고유한 아이디를 부여받은 모습이다.

 

그럼 만약 아래와 같이 구성하는 경우에는 어떨까?

export default function MainPage() {
  return (
    <section>
      <InputComponent />
    </section>
  )
}

function InputComponent() {
  const id = useId()
  return (
    <div>
      <label htmlFor={id}>이름</label>
      <input id={id}></input>
      <label htmlFor={id}>나이</label>
      <input id={id}></input>
    </div>
  )
}

위 예제에서는 나이라는 label과 input을 추가했다.

그리고 useId를 하나만 호출해서 사용했다.

 

그러면 아래 GIF와 같이 "나이" label을 클릭하면 "이름"의 input으로 focus된다.

이 문제는 useId()를 두개 만들어서 해결해도 된다.

 

🛠️useId 효울적으로 사용하는 방법!

하지만 더 개발자스럽게 해결하는 방법이 있다.

function InputComponent() {
  const id = useId()
  return (
    <div>
      <label htmlFor={`${id}-name`}>이름</label>
      <input id={`${id}-name`}></input>
      <br />
      <label htmlFor={`${id}-age`}>나이</label>
      <input id={`${id}-age`}></input>
    </div>
  )
}

이렇게 모든 Input과 label에 고유한 아이디를 부여할 수 있게 됐다.

 

 

근데 보통은..

우리는 보통 useId() 존재를 몰랐을 뿐더러 useId가 아니더라도 아래와 같이 고유한 아이디를 부여하는 방법은 많았다.

 

보통 사용하고 있는 방법을 찾아봤다.

1. Date 객체를 이용한 Timestamp 생성

function generateIdWithTimestamp() {
  return new Date().getTime();
}

이 방법은 매우 간단하며, 생성된 ID는 시간 기반으로 고유합니다.

하지만, 매우 빠른 속도로 ID를 생성할 경우 중복될 가능성이 있습니다.

2. Math.random()을 이용한 난수 생성

function generateRandomId() {
  return Math.random().toString(36).substr(2, 9);
}

 

Math.random()은 0 이상 1 미만의 부동소수점 난수를 생성합니다. 이를 문자열로 변환하고, substr을 사용하여 일정 길이의 문자열을 추출합니다. 이 방법은 간단하고 빠르지만, 완벽히 고유함을 보장할 수는 없습니다.

3. UUID 생성

UUID(Universally Unique Identifier)는 고유성을 보장하는 128비트 길이의 식별자입니다. JavaScript에서는 다음과 같은 라이브러리를 사용하여 UUID를 생성할 수 있습니다.

// uuid 라이브러리를 설치해야 합니다.
// npm install uuid

import { v4 as uuidv4 } from 'uuid';

function generateUUID() {
  return uuidv4(); // 예: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
}

 

이 방법은 매우 안정적이며, 생성된 ID는 전역적으로 고유함이 보장됩니다.

4. Crypto API를 이용한 안전한 난수 생성

브라우저 환경에서는 Crypto API를 사용하여 안전한 난수를 생성할 수 있습니다.

function generateSecureRandomId() {
  const array = new Uint32Array(1);
  window.crypto.getRandomValues(array);
  return array[0].toString(36);
}

이 방법은 보안 측면에서 뛰어나며, 생성된 난수는 예측하기 어렵습니다.

 

 

왜 리액트에서 useId를 제공하는 걸까?

그러면 위 방법대신 useId를 사용하는 이유가 무엇일까?

 

useId의 장점이 있기 때문이다.

 

아까 useId를 console.log()로 출력하면 아래와 같이 나왔다.

여기서 콜론이 흥미로운 역할을 한다.

 

예제로 설명해보려고한다

function InputComponent() {
  const id = useId()

  useEffect(() => {
    const element = document.querySelector(`#${id}`)
    console.log(element)
  }, [])

  return (
    <div>
      <button id={id}>버튼</button>
    </div>
  )
}

querySelector를 이용해서 요소에 접근하려고 한다.

근데 아래와 같은 오류가 뜬다.

이게 왜 이러냐면 요소의 id에 콜론이 붙어있으면 querySelector와 같은 API들이 잘 동작하지 않는다.

그래서 에러가 뜨는 것이다.

 

근데 이게 왜 장점일까?

사실 리액트에서는 DOM요소에 접근할 때 ref를 이미 제공해주고 있다.

그렇기 때문에 리액트를 사용한다면 굳이 querySelector 와 같은 API를 사용할 필요가 없다.

 

아래 ref를 사용한 예제다.

function InputComponent() {
  const id = useId()
  const ref =useRef(null);
  useEffect(() => {
    const element = ref.current
    console.log(element)
  }, [])

  return (
    <div>
      <button id={id} ref={ref}>버튼</button>
    </div>
  )
}

 

우리가 리액트를 사용할 때는 querySelector를 사용하는 것은 좋지않은 방법이다.

그래서 useId는 콜론을 아이디에 넣어줌으로써 리액트 개발자가 좀 더 나은 코드를 작성하도록 도와주는 것이다.

 

 

두 번째 장점은 안정성이다.

 

만약 아래와 같이 Math.radom()을 하게 되면 컴포넌트가 렌더링 될 때 마다 Id가 새로운 값으로 변경된다.

이렇게 id가 계속해서 바뀌게 된다면 스크린 리더가 반복적으로 label 읽게 돼서 불편해진다고 한다.

 

반면에 useId로 만들어진 Id는 컴포넌트 렌더링이 발생해도 Id가 그대로 유지된다.

렌더링이 되어도 바뀌지 않는 모습

그렇기 때문에 form요소에 있는 id도 유지된다.

 

useId의 안정성은 SSR에서 빛을 발한다!

이 안정성은 서버사이드렌더링에서 큰 역할을 하게 된다.

 

페이지를 서버에서 렌더링한 후에 클라이언트로 전송한다.

클라이언트에서는 받아온 페이지를 하이드레이션이라는 과정을 통해서 상호작용이 가능한 페이지로 만든다.

 

서버사이드렌더링으로 개발을 하다보면 

서버에서 렌더링된 결과물클라이언트에서 렌더링된 결과물일치하지 않아서 

문제가 발생하는 경우가 있다.

 

서버에서 렌더링된 form 요소의 id 값과

클라이언트에서 렌더링된 form요소의 id값이 일치하지 않을 때 문제가 생기는 것이다.

 

그렇지만 useId를 사용하면 서버와 클라이언트에서 모두 동일한 Id를 생성하기 때문에 이러한 문제를 방지할 수 있다!

 

 

 

 

📖useTransition

useTransition은 UI를 차단하지 않고 상태를 업데이트할 수 있는 React Hook입니다.

const [isPending, startTransition] = useTransition()

 

쉽게 말하면 상태 업데이트의 우선순위를 낮추는 것이다.

 

아래 예제를 보자

export default function MainPage() {
  const [isPending, startTransition] = useTransition()
  const [result, setResult] = useState([])
  const [name, setName] = useState('')

  const handleChange = e => {
    setName(e.target.value)
    startTransition(() => {
      setResult(e.target.value + '데이터가 입력됐어요')
    })
  }

  return (
    <section style={{ margin: '100px' }}>
      <input
        style={{ backgroundColor: 'white', color: 'black' }}
        value={name}
        onChange={handleChange}></input>
      {isPending && <div>로딩중...</div>}
      {name
        ? Array(1000)
            .fill('')
            .map((v, i) => <div key={i}>{result}</div>)   //무거운 작업을 하는 곳
        : null}
    </section>
  )
}

예제에서 보이듯 setResult를 통해서 나오는 결과는 무거운 작업을 하도록 했다.

그래서 setResult의 우선순위를 낮추고 무거운 연산이 setName에 영향이 없도록 하는 것이다.

 

아래 실행결과를보자

input의 값이 바뀌고 실시간으로 바뀌는게 아닌

로딩중이 뜬 이후 아래 result값에 반영되고 있다.

 

즉 무거운 연산을 뒤로 미룬 것이다.

 

만약 useTransition을 사용하지않고 하면 어떻게 될까?

  const handleChange = e => {
    setName(e.target.value)
    setResult(e.target.value + '데이터가 입력됐어요')
  }

아래 실행 결과를 보자

타이핑 자체도 렉이 걸리듯이 잘 되지 않았고,

아래 결과도 같이 렌더링 되다보니 useTransition을 사용할 때 보다 더 끊기는 느낌을 받았다.

 

이처럼 무거운 연산이 input에 까지 영향이 가서 사용자 경험이 안 좋아졌다.

 

이를 통해서 useTransition을 사용하여 사용자 입력과 같은 빠른 이벤트 처리와 무거운 데이터 처리 작업을 동시에 수행하면서도, 애플리케이션의 반응성을 유지할 수 있는 것이다.

 

 

📖useDeferedValue

이 메서드는 useTransition과 비슷하다.

보통 이 메서드는 useMemo와 사용한다.

어떤 상태가 아닌 값이 무거운 연산을 할 때 사용하는 것이다.

 

아래 예제를 보자

result에 useMemo를 두고 name값이 바뀔 때마다 재생성되도록 했다.

그리고 result를 1000개의 데이터로 출력하게 했다.(무거운 연산)

export default function MainPage() {
  const [name, setName] = useState('')
  const result = useMemo(() => name + '이 입력됐어요', [name]) //무거운 연산

  const handleChange = e => {
    setName(e.target.value)
  }

  return (
    <section style={{ margin: '100px' }}>
      <input
        style={{ backgroundColor: 'white', color: 'black' }}
        value={name}
        onChange={handleChange}></input>
      {name
        ? Array(1000)
            .fill('')
            .map((v, i) => <div key={i}>{result}</div>)
        : null}
    </section>
  )
}


이 실행결과를 보자

아까 useTransition을 사용하지 않을 때 처럼 타이핑을 빨리하면 해당 값이 실시간으로 보이지 않고 한번에 입력이 보이게 되면서 아래 result가 출력된다.

즉 입력이 끊기게된다.

 

그럼 useDeferredValue를 사용해보자

아래와 같이 deferredValue에 useDeferredValue(name)을 저장하고 

result에서는 deferredValue가 바뀔 때 재생성 되도록 했다.

여기서 result는 리액트에서 성능에 여유가 있을 때 처리되는 것이다.

export default function MainPage() {
  const [name, setName] = useState('')
  const deferredValue = useDeferredValue(name)
  const result = useMemo(() => deferredValue + '이 입력됐어요', [deferredValue]) // 우선순위가 낮아서 리액트에서 성능에 따라 여유있을 때 처리한다.

  const handleChange = e => {
    setName(e.target.value)
  }

  return (
    <section style={{ margin: '100px' }}>
      <input
        style={{ backgroundColor: 'white', color: 'black' }}
        value={name}
        onChange={handleChange}></input>
      {name
        ? Array(1000)
            .fill('')
            .map((v, i) => <div key={i}>{result}</div>)
        : null}
    </section>
  )
}

 

실행 결과를 보자

보이는 것처럼 입력을 빠르게 해도 실시간으로 input에 입력되는 것을 볼 수 있다.(처음에 입력할 때만 렉이 생김)

즉 무거운 연산이 input의 onChange에 따라 set함수를 수행하는 것에 영향을 주지 않는 것이다.

무거운 연산은 deferredValue로 인해 우선순위가 낮아진 것이다.

 

이 방식을 통해, 사용자 입력 처리와 같은 우선순위가 높은 작업을 빠르게 처리할 수 있으며, 상대적으로 우선순위가 낮은 리스트 필터링 작업은 React가 시스템 리소스가 허용하는 대로 처리하게 된다.

결과적으로, 사용자는 입력 중에도 UI가 버벅이지 않는 원활한 경험을 할 수 있는 것이다.

728x90
반응형
LIST