ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] 18버전의 추가된 새로운 기능 (step- 1)
    프로그래밍/React 2024. 2. 17. 19:59

    📋 개요

    • useId
    • startTransition, useTransition
    • useDefferedValue
    • useInesrtoinEffect

     

     

     


    ✅ useId

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

     

    📦 사용하는 이유)

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

    예시)

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

     

    📦 사용 예시)

    • 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. 사용이 매우 한정적이므로 필요한지 여부를 잘 확인하고 사용해야 한다.

     

     

     

     

     

    댓글

Designed by Tistory.