CS/프레임워크&라이브러리

[React] 18버전의 추가된 새로운 기능 (step- 1) - 강신범

고래강이 2024. 2. 17. 20:00

📋 개요

  • useId
  • startTransition, useTransition
  • useDefferedValue
  • useSyncExternalStore
  • useInesrtoinEffect

 

 

 


✅ useId

최상위 수준에서 호출되어 고유 ID를 생성하는 React Hook으로, 접근성 속성에 전달될 수 있다.
  • key 는 데이터 식별을 위해 사용되므로 데이터에서 생성되는 것이 더 바람직하며, 너무 많은 호출이 일어나기 때문이다.

 

📦 사용하는 이유)

  1. 하드코딩의 횟수를 줄이자
  2. 컴포넌트를 여러번 사용하더라도 id 속성이 겹치지 않는다.
  3. hydration mismatch를 해결할 수 있다.
더보기

예시)

사용자의 이름 정보를 받기 위해 input을 컴포넌트로 받아서 id값을 name이라고 하였다. 이때 이 컴포넌트를 반복해서 사용하게 된다면 id가 name인 input이 여러 개 생기게 되는데 이는 좋은 상황이 아니다. 그렇기에 이러한 id 속성의 중복을 막을 수 있다. 또한 id를 설정을 할 때 일일히 하드 코딩으로 구현하는 것은 바람직하지 않다고 react-ko에서 말한다.

또한 무작위로 id값을 생성하는 경우에 발생할 수 있는 hydration mismatch를 해결할 수 있다.

 

📦 사용 예시)

  • input을 반복해서 많이 사용하는 경우에 사용할 수 있으며 회원가입이나 로그인 상황에서 에러문구를 띄울 때 유용하게 사용될 것이다.
더보기
import { useId } from 'react';

function InputField() {
  const id = useId();
  return (
    <>
      <label>
        ID:
        <input
          type="text"
          aria-describedby={id}
        />
      </label>
      <p id={id}>
        The ID should contain at least 18 characters
      </p>
    </>
  );
}

 

 


✅ starTransition, useTransition 🔗

isPending과 startTransition 두개의 항목이 있는 배열을 반환하는 훅으로 UI를 차단하지 않고 상태를 업데이트 할 수 있다.
  • startTransition()을 통해 우선순위가 낮은 상태 업데이트들을 transition이라고 표기해 우선순위를 정해준다. (transition이라고 적히면 낮은 우선순위)
  • 낮은 우선순위를 가진 상태는 다른 상태 업데이트가 호출되면 중단된다.

 

📦 사용하는 이유)

  1. 느린기기에서도 사용자 인터페이스 업데이트를 반응성 있게 유지할 수 있다.
  2. UI 랜더링 시 우선순위에 따라 업데이트를 할 수 있다.
  3. Suspense와 연계하여 불필요한 로딩 인디케이터(Spinner와 같은 것)의 노출을 막을 수 있어 UX를 개선시킨다.
더보기

예시)

느린 기기에서 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하게 되었을 때 첫 번째 리렌더링이 완료될 때까지 기다린 이후 다음 클릭할 수 있었을텐데 Hook의 사용을 통해서 기다릴 필요 없이 다른 탭을 클리갛ㄹ 수 있게 되었다.

 

📦 사용 예시)

  • 느린 사용자 기기에서 탭의 전환 시에 사용하면 유용하다.
더보기
const App = () => {
  const [tab, setTab] = useState('about');

  return (
    <>
      {/* 탭을 클릭하면 렌더링할 탭 컴포넌트가 설정된다 */}
      <TabButton isActive={tab === 'about'} onClick={() => setTab('about')}>
        About
      </TabButton>
      <TabButton isActive={tab === 'posts'} onClick={() => setTab('posts')}>
        Posts (slow)
      </TabButton>
      <TabButton isActive={tab === 'contact'} onClick={() => setTab('contact')}>
        Contact
      </TabButton>
      <hr />
      {/* 현재 탭에 따라 탭 컴포넌트가 렌더링 된다 */}
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
};

const TabButton = ({ children, isActive, onClick }) => {
  const [isPending, startTransition] = useTransition();

  // 현재 탭이 활성화 되면 isActive 상태가 된다.
  if (isActive) {
    return <b>{children}</b>;
  }
  // 대기 중인 transition이 있다면 isPending이 된다.
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  /**
   * props로 받은 onClick 함수를 startTransition으로 감싸주기 때문에
   * onClick 함수(setTab)은 transition으로 설정되어 렌더링시 우선순위에서 밀리게 된다.
   * 그 결과 오랜시간이 걸리는 PostsTab 컴포넌트를 렌더링 하는 도중 다른 탭을 누르게 되면
   * PostsTab 컴포넌트의 렌더링을 멈추고 다른 컴포넌트를 렌더링하게 된다.
   **/
  const handleButtonClick = () => {
    startTransition(() => {
      onClick();
    });
  };
  return <button onClick={handleButtonClick}>{children}</button>;
};

const AboutTab = () => {
  return <p>Welcome to my profile!</p>;
};
const PostsTab = () => {
  const startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // 1 ms 동안 아무것도 하지 않음으로써 매우 느린 코드를 실행한다.
  }
  return <p>PostsTab</p>;
};
const ContactTab = () => {
  return <p>ContactTab</p>;
};
const ContactTab = () => {
  return <p>ContactTab</p>;
};

 

📦 주의 사항)

1️⃣ startTransition

  1. 동기 함수여야 한다.
  2. transition으로 표시된 setState는 다른 setState 업데이트 시 중단된다.
    • 다른 상태 업데이트가 있을 경우 우선 순위에서 밀린다.
  3. 텍스트 입력을 제어하는데 사용할 수 없다.
    • input에 사용하게 되면 사용자의 입력이 즉각적으로 반영이 되지 않는 경우도 생기기에 좋지 못한 선택이다.

 

 

 


✅ useDefferedValue 🔗 

UI 일부의 업데이트를 지연시킬 수 있는 React Hook이다.
  • 지연시키려는 값을 매개변수로 받으며, 초기 랜더링 시에는 매개변수로 제공한 값과 동일하며, 업데이트가 발생하면 백그라운드에서 새 값으로 리랜더링을 시도하기 전까지 이전 값을 반환하여 UX를 개선시킨다.

 

📦 사용하는 이유)

  1. Debounce와 비슷한 효과를 낼 수 있으며, 그보다 뛰어난 성능을 보인다.
  2. Suspense와 통합되어 새 값으로 인한 백그라운드 업데이트로 UI가 일시 중단되면 fallback이 표시되지 않고 기존의 값이 유지되어 UX에 좋다.

 

📦 사용 예시)

  • 검색을 통해서 리스트를 가져오는 상황에서 다른 검색동안 이전 값을 유지하는데 유용하게 사용될 수 있다.(Debouncing + a).
더보기
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

 

📦 주의 사항)

  1. 전달하는 값은 원시값이거나 컴포넌트의 외부에서 생성된 객체여야 한다.
    • 랜더링 중에 생성되는 객체(컴포넌트 안에서 생성되는 객체)를 생성하고 즉시 전달하면 렌더링 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있다.
  2. 사용하는 자체로 추가 네트워크 요청을 방지하지 않기에 주의해야 한다.
  3. Effect의 실행은 useDefferedValue로 인해 백그라운드 리랜더링이 완료되고 화면에 커밋되어야 실행된다.

 

 

 


✅ useSyncExternalStore 🔗

외부 스토어를 구독할 수 있는 React Hook 즉, 외부 스토어(external store)와 싱크(sync)를 맞추는 훅(use)이다.
  • concurrnet 렌더링이 등장하면서 렌더링이 렌더링을 일시중지할 수 있게되었는데 이 일시 중지가 발생하는 문제로 인해 UI에 동일한 데이터가 다른 값을 표시하는 경우가 생겨났고 이러한 문제를 해결하는 방법이다.
  • Redux와 MobX와 같은 외부 상태 저장소와 React와의 동기화를 쉽고 안정적으로 수행할 수 있도록 한다.
  • 옵저버 패턴을 이용하며 컴포넌트가 store를 구독하게 하여 store가 바뀔 때마다 컴포넌트를 리렌더링하게 할 수 있다.

 

 

📦 사용하는 이유)

  1. concurrent Mode의 문제점을 지속적인 외부 상태와 React 컴포넌트 사이의 상태 동기화를 통해 해결해 줄 수 있다.
더보기

예시)

리액트 뿐만아니라 다양한 라이브러리 및 프레임워크가 사용된 프로젝트가 있다. 이때 여기서 사용하고 있는 스토어(외부 라이브러리)와 리액트에 상태를 통합해서 사용하기 위해서 사용하며, 주로 기존의 비 React 코드와 통합을 할 때 유용하다.

 

📦 사용 예시)

  • 필수로 2개의 매개변수와 선택적으로 1개의 매개변수가 있다.
  • subscribe의 경우 함수 내에서 구독과 구독취소를 모두 선언해주어야 한다.
  • getSnapshot는 스토어 데이터의 스냅샷을 반환하는 함수로 저장소가 변경되어 반환값이 달라지면 컴포넌트를 리랜더링 한다.
  • getServerSnapshot은 데이터의 초기 스냅샷을 반환하는 함수로 hydrate하는 동안에만 사용된다.
더보기
// App.js

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}


// todoStore.js

let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}

 

 

 

📦 주의 사항)

  1. snapshot을 캐시해야한다는 오류가 발생할 수 있다.
    • 이는 함수가 호출될 때마다 새 객체를 반환한다는 의미이며, getSnapshot 함수는 반환값이 지난번과 다르면 컴포넌트를 리랜더링하기에 실제 변경 사항이 있을 때에만 다른 객체를 반환해야 한다.
  2. subscribe 함수는 컴포넌트 안에 만들면 안된다.
    • subscribe 함수는 실제 변화가 있을 시에만 새롭게 전달되야한다. 그렇지 않으면 구독과 구독취소를 반복해서 일으켜 무한루프에 빠질 수 있다.

 

 

 


✅ useInsertionEffect

useEffect의 버전 중 하나로 DOM 변이 전에 실행된다.
  • Effect 로직이 포함되어 컴포넌트가 DOM에 추가되기 전에 setup 함수를 실시하고 제거되기 전에 cleanup 함수를 실행한다.
  • undefined를 반환한다.
  • 동적으로 스타일을 삽입하는 경우에 사용된다.

 

 

📦 사용하는 이유)

1️⃣ CSS-in-JS 라이브러리에서 동적 스타일 삽입하기

일반적으로 사용되는 접근 방식은 3가지가 있다.
1. 컴파일러 사용해서 CSS 파일로 정적 추출
2. 인라인 스타일로 적용 ex) <div style={{ opacity: 1 }}> 
3. 런타임에서 <style> 태그 삽입 (권장되지 않음, 스타일 계산 빈도가 훨씬 증가하며, 잘못된 시점에 발생하면 속도 저하의 원인)

이 중 useInsertionEffect는 런타임에 스타일을 주입해야 하는 경우 2번째 문제를 해결하며 사용할 수 있는 방법이다.

 

2️⃣ 일반적으로 대부분의 개발자는 필요로하지 않는다.

  1. CSS-in-JS 라이브러리와 같은 제작자를 위해 설계됨

 

📦 주의 사항)

  1. 서버 랜더링 중에 실행되지 않고 클라이언트에서만 실행된다.
  2. 사용이 매우 한정적이므로 필요한지 여부를 잘 확인하고 사용해야 한다.

 

 

 

 

 

'CS > 프레임워크&라이브러리' 카테고리의 다른 글

[React] 18버전의 추가된 새로운 기능 (step- 2)  (0) 2024.02.24
React v18 - 2  (0) 2024.02.24
React v18 - 1  (0) 2024.02.17
프레임워크와 라이브러리  (2) 2024.01.26
리액트와 jQuery의 차이점  (0) 2023.11.02