sharingStorage

React18 new Hooks 본문

Front-End/React

React18 new Hooks

Anstrengung 2023. 12. 7. 23:56

useId

접근성 속성에 전달되는 특별한 ID를 만드는 hooks

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();

Usage

  • 접근성 attributes 를 위한 특별한 ID를 만들기 위해
  • 몇개의 관련된 elements 에 대한 ID를 만들기 위해
  • 모든 생성된 ID들에 대한 접두사를 지정하기 위해
  • 클라이언트와 서버가 같은 접두사 ID를 사용하기 위해

Parameters

useId는 어떠한 parameter도 존재하지 않습니다.

Returns

useId는 컴포넌트 내에 useId 호출과 관련된 특별한 ID string을 반환합니다.

주의 사항

  • useIdHook이므로 컴포넌트의 최상위 수준 이나 자체 Hook에서만 호출할 수 있습니다. 루프나 조건문 내에서는 호출할 수 없습니다. 필요한 경우 새 구성 요소를 추출하고 상태를 해당 컴포넌트로 옮깁니다.
  • useId 목록에서 키를 생성하는 데 사용해서는 안 됩니다 .

Ex)

<label>
  Password:
  <input
    type="password"
    aria-describedby="password-hint"
  />
</label>
<p id="password-hint">
  The password should contain at least 18 characters
</p>

위와 같이 ID하드코딩하는 것은 좋은 습관이 아닙니다. 컴포넌트는 한 페이지에서 여러번 렌더링될 수 있지만 ID는 고유해야하므로 useId를 사용할 수 있습니다.

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  return (
    <>
      <label>
        Password:
        <input
          type="password"
          aria-describedby={passwordHintId}
        />
      </label>
      <p id={passwordHintId}>
        The password should contain at least 18 characters
      </p>
    </>
  );
}
  • Aria-describedby

스크린리더 사용자에게 특정 요소의 상세 설명을 제공하거나, 기타 복잡하게 그룹화 된 레이아웃을 설명하는 용도로 사용하는 것.

모든 HTML 요소에 사용 가능하며 ARIA-labelledby보다 더 상세한 설명이 제공되어야할 때 사용.

접근성 api로는 aria 나 ally가 있습니다.

여러 ID생성하는 법

옳은 예

import { useId } from 'react';

export default function Form() {
  const id = useId();
  return (
    <form>
      <label htmlFor={id + '-firstName'}>First Name:</label>
      <input id={id + '-firstName'} type="text" />
      <hr />
      <label htmlFor={id + '-lastName'}>Last Name:</label>
      <input id={id + '-lastName'} type="text" />
      <label htmlFor={id + '-nickName'}>Last Name:</label>
      <input id={id + '-nickName'} type="text" />
    </form>
  );
}

틀린 예

import { useId } from 'react';

export default function Form() {
  const id = useId();
	const id2= useId();
	const id3= useId();
  return (
    <form>
      <label htmlFor={id}>First Name:</label>
      <input id={id} type="text" />
      <hr />
      <label htmlFor={id2}>Last Name:</label>
      <input id={id2 } type="text" />
  <hr />
      <label htmlFor={id3}>Nick Name:</label>
      <input id={id3} type="text" />
    </form>
  );
}

위와 같이 복수의 ID가 필요할 땐 여러번 생성하는 것이 아닌 id를 접두사로 사용해서 재사용합니다.

생성된 모든 ID에 대한 공유 접두사 지정

만약 여러분들이 싱글페이지에서 여러 독립적인 app을 렌더하려고 한다면 createRoot또는 hydrateRoot를 부를 때 identifierPrefix 옵션을 호출하면 됩니다.

이렇게하면 모든 식별자가 지정한 고유한 접두사로 시작하기 때문에 서로 다른 두 앱에서 생성된 ID가 충돌하지 않습니다.

//index.js
import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';

const root1 = createRoot(document.getElementById('root1'), {
  identifierPrefix: 'my-first-app-'
});
root1.render(<App />);

const root2 = createRoot(document.getElementById('root2'), {
  identifierPrefix: 'my-second-app-'
});
root2.render(<App />);
//index.html
<!DOCTYPE html>
<html>
  <head><title>My app</title></head>
  <body>
    <div id="root1"></div>
    <div id="root2"></div>
  </body>
</html>

클라이언트와 서버에서 동일한 ID 접두사 사용

동일한 페이지에서 여러 개의 독립적인 React 앱을 렌더링하고 이러한 앱 중 일부가 서버에서 렌더링되는 경우 클라이언트 측에서 hydrateRoot 호출에 전달하는 IdentifierPrefix가 서버 API에 전달하는 IdentifierPrefix와 동일해야합니다.

// Server
import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(
  <App />,
  { identifierPrefix: 'react-app1' }
);

// Client
import { hydrateRoot } from 'react-dom/client';

const domNode = document.getElementById('root');
const root = hydrateRoot(
  domNode,
  reactNode,
  { identifierPrefix: 'react-app1' }
);

동형 애플리케이션(Isomorphic)에서 useId hooks

여러분들은 id값을 고유하게 만들기 위해 Math.random 이나 uuid 관련 라이브러리를 사용한 경험이 있을텐데 위 두가지 방법으로 id를 사용하면 React에서 CSR과 SSR을 과정에서 id값이 일치하지 않는 문제가 발생합니다. 이러한 id 불일치는 hydration과 접근성이슈를 발생시키는데, 그 이유는 대부분에 접근성 API (accessibility API)는 컴포넌트에 연결된 id에 의존하기 때문입니다.

Isomorphic React app :

서버와 클라이언트 사이드 둘 다에서 동작하는 코드로 구성된 웹 앱입니다.

서버로부터 가져오는 SEO의 유리함과, 복잡한 유저 상호작용을 처리할 수 있는 브라우저의 능력이 결합되었습니다.

useId를 사용할 때

  • 고유한 ID생성
  • label 및 input과 같은 HTML요소를 연결

useId를 사용하면 안될 떄

  • list에 대한 key사용
  • CSS 선택자 사용 (useId가 콜론을 포함하는 문자열을 반환하기 때문)
  • 불변의 값일 때

 

 

 

useTransition

what?

useTransition은 UI blocking 없이 상태를 업데이트 시켜주는 React Hook입니다.

const [isPending, startTransition] = useTransition()

parameters

useTransition 은 parameters를 가지지 않습니다.

returns

useTransition은 두개의 요소가 담긴 배열을 리턴합니다.

isPending : 보류중인 transition이 존재하는지에 대한 boolean 값 (상태변화가 지연되고 있음을 알리는 값)

startTransition: transition 우선순위를 변경

(startTransition은 상태 변화를 일으키는 콜백함수를 전달받고 해당 콜백함수는 낮은 우선순위로 실행되게 된다.)

startTransition function

startTransition은 사용자에게 state update 를 transition으로써 표시할 수 있습니다.

 

why?

debounce, throttle 과의 비교

input에 text를 입력하고 그에 해당되는 화면을 출력하는 페이지를 구성할 때로 예를 들어보겠습니다.

debounce

debounce는 일정시간 뒤에 화면 업데이트를 할 수 있습니다.

그러나 입력이 계속된다면 화면업데이트 역시 계속 미뤄질 수 있습니다.

즉 단순히 문제를 뒤로 미루는 것일 뿐입니다.

throttle

throttling을 이용하여 3초주기로 화면을 그리게 할 수도 있지만 3초동안 유저가 입력하지 않으면 그 시간이 낭비됩니다. 따라서 이 역시 적합한 방법이 아닙니다.

startTransition

반면 startTransition을 사용하면 사용자 입력중에도 화면 업데이트를 계속할 수 있습니다.

위 두개랑 비교해서 비어있던 시간들이 사라지고 사용자 경험을 더 향상시킬 수 있습니다.

 

How

지연을 미루고 싶은 상태 업데이트 함수를 startTransition으로 감싸줍니다.

startTransition(() => {
    setText(e.target.value);
  });

isPending을 사용해서 지연이 되고 있다면 loading page나 스피너를 띄워줄 수도 있습니다.

const [isPending, startTransition] = useTransition();

  if (isPending) {
    return <b className="pending">로딩중 ...</b>;
  }
return (
	<div> 로딩 끝! </div>
)

 

 

다음 예시는 Posts탭에 접근하면 인위적으로 느려지게 render되는 페이지입니다.

게시물을 클릭한 후 바로 contact를 클릭하면 게시물의 렌더링이 중단됩니다. 이 상태 업데이트는 transition으로 감싸져서 느린 리렌더링이 있어도 UI가 멈추거나 기다리지 않습니다. 

 

Suspens & fallback

다음과 같이 Suspens도 지원돼서 loading fallback을 사용할 수도 있습니다

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>🌀 Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>

 

When?

블로킹 렌더 문제를 해결하기 위해

  • 한 번 렌더링 연산이 시작되면 멈출 수 없음
  • 대현 화면 업데이트의 경우 렌더링 되는 동안 페이지 지연 발생하기 때문에 그 우선순위를 나누고 높은 우선순위 이벤트를 먼저 처리하기 위해

 

 

useDeferredValue

What?

UI의 일부 업데이트를 연기할 수 있는 React Hook입니다.

Usage

  • 새로운 콘텐츠가 로딩되는 동안 오래된 콘텐츠를 표시할 때
  • 콘텐츠가 오래되었음을 나타낼 때
  • UI일부에 대해 리렌더링을 연기할 때
import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

Parameters

value : 연기하려는 값 : 어떤 타입도 가능합니다.

Returns

초기렌더링에선 반환된 지연값은 사용자가 제공한 값과 동일합니다.

업데이트 하는 동안에 처음엔 old value 를 리턴하고 그 다음엔 new value (update value)를 리턴합니다.

How

import { useDeferredValue, useState } from "react";

export default function Home() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [count3, setCount3] = useState(0);
  const [count4, setCount4] = useState(0);

  const deferredValue = useDeferredValue(count2);

  const onIncrease = () => {
    setCount1(count1 + 1);
    setCount2(count2 + 1);
    setCount3(count3 + 1);
    setCount4(count4 + 1);
  };

  console.log({ count1 });
  console.log({ count2 });
  console.log({ count3 });
  console.log({ count4 });
  console.log({ deferredValue });

위 예시와 같이 가장 낮은 순위로 상태를 변경하고 싶을 때 useDeferredValue로 값을 감싸주면 된다.

새로운 콘텐츠가 로드되는 동안 오래된 콘텐츠 표시

useDeferredValue를 사용하면 React는 지연된 값을 업데이트하지 않고 old value로 리렌더링한 후 백그라운드에서 새로 수신된 값(new value)으로 리렌더링을 시도합니다. 즉 loading중일때 이전 값이 표시됩니다.

when?

useTransition vs useDeferredValue

useTransition은 set함수를 callback으로 받아서 사용한다.

startTransition(() => {
    setText(e.target.value);
  });

useDeffered는 값을 래핑해서 사용한다.

const deferredValue = useDeferredValue(value);

두개의 hook은 결과적으로 동일한 목표를 달성하므로 두 가지를 함께 사용할 필요는 없습니다.

대체적으로 useTransition을 선호하는 것 같지만 만약 set함수(state update)에 대한 접근권한이 없다면 useDeferredValue를 사용해야합니다.

Comments