본문으로 건너뛰기

React useTransition

무료2019-11-23#JS#React useTransition#React useDeferredValue#React SuspenseList#React Suspense编排#React SuspenseList rfc

극치 경험을 추구하는 큰 길에서, React 는 loading 을 화려하게 연출했습니다

서론

Suspense 이후, useTransition 이 등장합니다

一.Suspense 만으로는 부족한가?

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Suspense 서브트리 내에 아직 돌아오지 않은 Lazy 컴포넌트가 존재하면, fallback 에서 지정한 콘텐츠로 진행하며, 임의의 조상 레벨의 loading 으로 끌어올릴 수 있습니다.

Suspense 컴포넌트는 (컴포넌트 트리 내의) Lazy 컴포넌트上方的 임의의 위치에 배치할 수 있으며,かつ下方에 복수의 Lazy 컴포넌트를 가질 수 있습니다.

loading 장면만 보면, Suspense 는 2 가지 능력을 제공합니다:

  • loading 의 끌어올리기를 지원

  • loading 의 집약을 지원

사용자 경험而言, 2 방면의 이점이 있습니다:

  • 레이아웃 지터 회피 (데이터가 돌아온 후 한 덩이의 콘텐츠가 나타남)

  • 서로 다른 네트워크 환경에 대한 대응 (데이터 돌아옴이 빠르면 loading 은 나타나지 않음)

전자는 loading(또는 skeleton) 에 의한 이점이고, 후자는 Concurrent Mode 하의 간헐 스케줄링에 의한 것입니다

P.S.Suspense 의 상세 정보는, React Suspense——코드 분할에 loading 을 추가하는 것에서부터…… 참조

Suspense 가 제공하는우아하고 유연, 인간성의 높은 loading 은 이미 극치의 개발 경험과 사용자 경험에 도달한 것처럼 보입니다가, 더욱 탐색하면, loading 을 둘러싸고 몇 가지 문제가 있습니다:

  • loading 을 추가하면, 경험은 반드시 좋아지는가?

  • 바로 loading 을 표시하는 것의 무엇이 문제인가?

  • 인터랙션의 실시간 응답과 loading 의 충돌을 어떻게 해결하는가?

  • 축소할 수 없는 긴 loading 에 대해, 사용자 지각상 더 빠르게 하는 방법은 있는가?

  • 레이아웃 지터는 정말 존재하지 않게 되었는가? 리스트 내에 복수의 loading 이 동시에 존재한다면?

다음으로, 이러한 문제들을 하나씩 검토합니다

二.시각적으로 loading 을 약화

loading 을 추가하면, 경험은 반드시 좋아지는가?

전형적인 페이지네이션 리스트를 예로 들면, 일반적인 인터랙션 프로세스는 다음과 같습니다:

1.1 페이지目の 콘텐츠가 표시
2. 다음 페이지를 클릭
3. 1 페이지目の 콘텐츠가 소실, 또는 반투명 레이어로 덮임
4. loading 이 표시
5. 잠시 후 loading 이 소실
6. 2 페이지目の 콘텐츠가 표시

그 중 최대의 문제는, loading 기간 중 1 페이지目の 콘텐츠가 사용 불가 (불가시, 또는 덮여 있음) 라는 것입니다. 즉, loading 이 페이지 콘텐츠의 완전성과, 애플리케이션의 응답 능력 (responsiveness) 에 영향을 미치고 있습니다

既然如此, 차라리 loading 을 삭제합시다:

1.1 페이지目の 콘텐츠가 표시
2. 다음 페이지를 클릭
3. 1 페이지目の 콘텐츠는 원래대로
...인터랙션 피드백 없이, 몇 초 후
4. 2 페이지目の 콘텐츠가 표시

즉시 인터랙션 피드백이 부족하기 때문에, 사용자 경험은 더욱 악화되었습니다. 그렇다면, 양립하는 방법 은 없을까요. loading 기간 중의 응답성을 보증하면서, loading 과 유사한 인터랙션 경험도 얻을 수 있는 방법입니다

있습니다. loading 의 시각 효과를 약화합니다:

  • 글로벌 loading(또는 콘텐츠 블록 loading) 을 로컬 loading 으로 약화: loading 이 콘텐츠 완전성을 파괴하는 것을 회피

  • 그레이아웃 등의 방법으로 표시 중이 구 콘텐츠임을 암시: 구 콘텐츠에 의한 사용자의 혼란을 회피

예를 들어, 버튼 클릭의 장면에서는, loading 피드백을 버튼 위에 간단히 추가할 수 있습니다:

//...
render() {
  const { isLoading } = this.state;

  return (
    <Page>
      <Content style={{ color: isLoading ? "black" : "gray" }} />
      <Button>{isLoading ? "Next" : "Loading..."}</Button>
    </Page>
  );
}

loading 프로세스 중 사용자가 보고 있는仍然是완전한 콘텐츠 (일부 콘텐츠는 조금 낡았지만, 그레이아웃으로 암시되어 있음) 것을 보증할 뿐만 아니라, 즉시 인터랙션 피드백도 제공할 수 있습니다

대부분의 경우, 위의 예처럼 바로 loading 을 표시해도 문제는 없지만, 다른 장면에서는, 신속하게 나타나는 loading 은 기대대로 아닙니다

三.논리적으로 loading 을 지연

바로 loading 을 표시하는 것의 무엇이 문제인가?

loading 이 매우 빠른 경우 (100ms 만), 사용자는 무언가가 순식간에 스쳐지나간 것만 느낄 수 있습니다……또 다른 나쁜 사용자 경험입니다

물론, 이러한 장면에서는 보통 loading 을 추가하지 않습니다. loading 은 보통 사용자에게 "느리다"는 심리적 기대를 가져오고, 본래 매우 빠른 조작에 loading 을 추가하면, 사용자 지각상의 속도 경험을 저하시키게 되므로, 추가하지 않는 것을 선택합니다

그러나, 매우 빠른 경우도 있고, 매우 느린 경우도 있는 조작이 있는 경우, loading 은 추가해야 하는가 추가하지 않아야 하는가?

이 때按需 loading 이 필요합니다. 예를 들어 loading 타이밍을 지연하고, 200ms 후 새 콘텐츠가 아직 준비되어 있지 않은 경우에만 loading 을 표시

React 는 이 장면을 고려했고, useTransition 이 탄생했습니다

useTransition

Transition 특성은 Hooks API 형식으로 제공됩니다:

const [startTransition, isPending] = React.useTransition({
  timeoutMs: 3000
});

P.S.주의, Transition 특성은 Concurrent Mode 에 의존하며,かつ현재 (2019/11/23) 는 정식으로 출시되지 않았습니다 (실험적 특성), 구체적인 API 는 더욱 변할 가능성이 있습니다, 참고만, 시연은 Transitions 참조

Transition Hook 의 역할은, State 의 업데이트를 지연해도 문제없다고 React 에게 전하는 것입니다:

Wrap state update into startTransition to tell React it's okay to delay it.

예를 들어:

function App() {
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = React.useTransition({
    timeoutMs: 3000
  });

  return (<>
    <button
      disabled={isPending}
      onClick={() => {
        startTransition(() => {
          const nextUserId = getNextId(resource.userId);
          setResource(fetchProfileData(nextUserId));
        });
      }}
    >
      Next
    </button>
    {isPending ? " Loading..." : null}
    <ProfilePage resource={resource} />
  </>);
}

function ProfilePage({ resource }) {
  return (<Suspense fallback={<h1>Loading profile...</h1>} >
    <ProfileDetails resource={resource} />
    <Suspense fallback={<h1>Loading posts...</h1>} >
      <ProfileTimeline resource={resource} />
    </Suspense>
  </Suspense>);
}
  1. Next 버튼을 클릭하여 즉시 ProfileData 를 취득, 그 후 isPendingtrue 가 되고, Loading... 이 표시

  2. ProfileData 가 3 초 이내에 돌아오면, (표시 중인 구 ProfilePage 에서 전환하여) 신 ProfilePage 콘텐츠를 표시

  3. 그렇지 않으면 ProfilePage 의 Suspense fallback 에 들어가고, (구 ProfilePage 가 소실) Loading profile... 이 표시

즉, startTransition 은 본래 즉시 ProfilePage 에 건네져야 할 (아직 취득하지 못한) resource 상태 값을 지연시키고, 최대 3 초 지연합니다. 이것이 바로 우리가 원하는按需 loading 능력입니다: timeoutMs 밀리초 내에는 loading 하지 않고, 타임아웃 후에야 loading 을 표시

따라서, 간단히 말하면, Transition 은 Suspense 를 delay 할 수 있습니다. 즉, Transition 은 loading 을 지연할 수 있습니다

按需 loading

페이지 콘텐츠 상태에서 보면, Transition 은구 콘텐츠仍然사용 가능한 Pending 상태를 도입했습니다:

각 상태의 의미는 다음과 같습니다:

  • Receded(소실): 현재 페이지 콘텐츠가 소실, Suspense fallback 으로 강등

  • Skeleton(스켈레톤): 새 페이지가 이미 나타나, 일부 새 콘텐츠仍然加载中

  • Pending(대기 중): 새 콘텐츠가 진행 중, 현재 페이지 콘텐츠는 완전,仍然인터랙션 가능

Pending 을 제안한 출발점은후퇴 (이미 존재하는 콘텐츠를 숨기는 것) 를 회피하는 것입니다:

However, the Receded state is not very pleasant because it "hides" existing information. This is why React lets us opt into a different sequence (Pending → Skeleton → Complete) with useTransition.

간단히 loading 을 추가한 경우의 대응하는 상태 변화는 Receded → Skeleton → Complete(빠른지 느린지와 관계없이, loading 을 표시) 이고, Transition 有了後, 체험이 최적인 상황은 Pending → Skeleton → Complete(매우 빠르고, loading 은 불필요), 조금 나쁜 것은 Pending → Receded → Skeleton → Complete(매우 느리고, loading 하지 않을 수 없음)

따라서, 최적의 체험을 위해, Pending 시간을 단축하고, 가능한 한 빨리 Skeleton 상태에 들어가야 합니다. 작은 테크닉은느린, 및 중요하지 않은 컴포넌트를 Suspense 로 감싸는 것입니다:

Instead of making the transition shorter, we can "disconnect" the slow component from the transition by wrapping it into

모범 사례

동시에, Hooks 의 세밀한 논리 재사용 방면의 이점 에 의해, 쉽게 Transition 의按需 loading 효과를 기초 컴포넌트에 캡슐화할 수 있습니다. 예를 들어 Button:

function Button({ children, onClick }) {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000
  });

  function handleClick() {
    startTransition(() => {
      onClick();
    });
  }

  const spinner = (
    // ...
  );

  return (<>
    <button onClick={handleClick} disabled={isPending}>
      {children}
    </button>
    {isPending ? spinner : null}
  </>);
}

이것도 공식 권장하는做法로, UI 컴포넌트 라이브러리가 useTransition 이 필요한 장면을 고려하는 것으로, 중복 코드를 축소합니다:

Pretty much any button click or interaction that can lead to a component suspending needs to be wrapped in useTransition to avoid accidentally hiding something the user is interacting with.

This can lead to a lot of repetitive code across components. This is why we generally recommend to bake useTransition into the design system components of your app.

四.인터랙션의 실시간 응답과 loading 의 충돌을 해결

인터랙션의 실시간 응답과 loading 의 충돌을 어떻게 해결하는가?

Transition 이 loading 표시를 지연할 수 있는 것은, State 업데이트를 지연했기 때문입니다. 그렇다면 지연할 수 없는 State 업데이트의 경우는 어떨까요. 예를 들어 입력값:

function App() {
  const [query, setQuery] = useState(initialQuery);

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value);
  }

  return (<>
      <input value={query} onChange={handleChange} />
      <Suspense fallback={<p>Loading...</p>}>
        <Translation input={query} />
      </Suspense>
  </>);
}

여기서 input 을 [제어 컴포넌트](/articles/从 componentwillreceiveprops 说起/#articleHeader5) 로서 사용하고 있습니다 (onChange 로 사용자 입력을 처리). 따라서 새 value 를 즉시 State 에 업데이트해야 합니다. 그렇지 않으면 입력 지연,さらには혼란이 발생합니다

따라서, 충돌이 발생했습니다. 이 실시간 응답 입력의 요구와 Transition 의 State 업데이트 지연은 공존할 수 없어 보입니다

공식이 제공하는 해결책은 이 상태 값을 중복화하는 것입니다. 충돌이 있다면, 차라리 나누어 각각 사용합니다:

function App() {
  const [query, setQuery] = useState(initialQuery);
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = useTransition({
    timeoutMs: 5000
  });

  function handleChange(e) {
    const value = e.target.value;

    // Outside the transition (urgent)
    setQuery(value);

    startTransition(() => {
      // Inside the transition (may be delayed)
      setResource(fetchTranslation(value));
    });
  }

  return (<>
      <input value={query} onChange={handleChange} />
      <Suspense fallback={<p>Loading...</p>}>
        <Translation resource={resource} />
      </Suspense>
  </>);
}

React 의 실천 경험은 계산할 수 있으면 계산하고, 공유할 수 있으면 공유하며, 상태 값을 중복화하지 않도록 가르��줍니다. 이점은 상태 업데이트 시의 누락을 회피할 수 있는 것입니다:

This lets us avoid mistakes where we update one state but forget the other state.

而我们刚刚也确实 중복화了一个 상태 값 (queryresource), 실천 원칙을 뒤집는 것이 아니라, State 에 우선순위를 구분할 수 있다는 것입니다:

  • 고우선 State: 업데이트를 delay 하고 싶지 않은 State. 예를 들어 입력값

  • 저우선 State: delay 가 필요한 상태. 예를 들어 Transition 관련

즉, Transition 有了後, State 에 우선순위가 생겼습니다

五.UI 일관성의 희생을 고려

축소할 수 없는 긴 loading 에 대해, 사용자 지각상 더 빠르게 하는 방법은 있는가?

있습니다. UI 일관성을 희생할 의사가 있다면

듣기 실수가 아닙니다. UI 일관성도 흔들릴 수 없는 것이 아니며, 필요시에는UI 일관성을 희생하여 지각상 더 좋은 체험 효과를 얻는것을 고려할 수 있습니다. "제목과 맞지 않는" 상황이 발생하지만, 10 초 또는 그 이상 loading 을 표시하는 것보다 우호적인 경우도 있습니다. 마찬가지로, 그레이아웃 암시 등의 수단으로 사용자에게 UI 불일치의 사실을 인식시킬 수 있습니다

为此, React 는 DeferredValue Hook 을 제공합니다

useDeferredValue

const deferredResource = React.useDeferredValue(resource, {
  timeoutMs: 1000
});

// 用法
<ProfileTimeline
  resource={deferredResource}
  isStale={deferredResource !== resource} />

P.S.주의, 현재 (2019/11/23) 는 useDeferredValue 가 정식으로 출시되지 않았습니다, 구체적인 API 는 더욱 변할 가능성이 있습니다, 참고만, 시연은 Deferring a Value 참조

Transition 機制와 유사하며, 상태 업데이트를 지연하는 것에相當하며, 새 데이터가 준비되기 전에, 구 데이터를 계속 사용할 수 있습니다. 1 초 이내에 새 데이터가 오면, (구 콘텐츠에서 전환하여) 새 콘텐츠를 표시하고, 그렇지 않으면 즉시 상태를 업데이트하고, loading 해야 하면 loading 합니다

Transition 과의 차이는, useDeferredValue 는 상태 값面向이고, Transition 은 상태 업데이트 조작面向이며, API 및 의미상의 차이이고, 機制上二者는 매우 닮았습니다

六.레이아웃 지터를彻底하여消除

레이아웃 지터는 정말 존재하지 않게 되었는가? 리스트 내에 복수의 loading 이 동시에 존재한다면?

복수 loading 병존의 장면에서는, loading 의先後順序가 다른 것에 의한 레이아웃 지터가 발생하기 쉽습니다. 시각 효과上, 보통 원래의 한 덩이의 것이 한쪽으로 밀려나는 것을 원하지 않습니다 (시각적으로는 append 여야 하며, insert 여서는 안 됩니다). 레이아웃 지터를彻底하여消除하려면, 2 가지思路가 있습니다:

  • 모든 리스트 항목을 동시 표시: 모든 항목이 준비될 때까지 기다린 후 표시하지만, 대기 시간이 올라감

  • 리스트 항목을 그 상대 순서에 따라 나타나게 함: insert 를消除할 수 있고, 대기 시간도 항상 최악은 아님

그렇다면, 비동기 콘텐츠의 출현 (loading 소실) 순서를 어떻게 제어하는가?

React 도 고려했으며, SuspenseList 를 제공하여 Suspense 콘텐츠의 렌더링 순서를 제어하고, 리스트 내 요소의 표시 순서가 상대 위치에 따르는 것을 보증하며, 콘텐츠가 밀려나는 것을 회피합니다:

<SuspenseList> coordinates the "reveal order" of the closest <Suspense> nodes below it

SuspenseList

import { SuspenseList } from 'react';

function ProfilePage({ resource }) {
  return (
    <SuspenseList revealOrder="forwards">
      <ProfileDetails resource={resource} />
      <Suspense fallback={<h2>Loading posts...</h2>}>
        <ProfileTimeline resource={resource} />
      </Suspense>
      <Suspense fallback={<h2>Loading fun facts...</h2>}>
        <ProfileTrivia resource={resource} />
      </Suspense>
    </SuspenseList>
  );
}

revealOrder="forwards"SuspenseList 아래의 자급 Suspense 가 위에서 아래로의 순서로 나타날 필요가 있음을 나타내며, 누구의 데이터가 먼저 준비되어도 마찬가지입니다. 유사한 값에 backwards(역순 출현) 와 together(동시 출현) 가 있습니다

또한, 복수의 loading 이 동시에 나타나는 것에 의한 사용자에게의 체험 혼란을 회피하기 위해, tail 옵션도 제공하고 있습니다. 상세는 SuspenseList 참조

P.S.주의, 현재 (2019/11/23) 는 SuspenseList 가 정식으로 출시되지 않았습니다, 구체적인 API 는 더욱 변할 가능성이 있습니다, 참고만, 시연은 SuspenseList 참조

七.정리

우리가 목격한 바와 같이, 극치 경험을 추구하는 큰 길에서, React 는 점점 더 멀리까지 나아가고 있습니다:

  • Suspense: 우아하고 유연, 인간성의 높은 콘텐츠 강등을 지원

  • useTransition: 按需 강등을 지원하며, 정말로 느린 경우에만 강등

  • useDeferredValue: UI 일관성을 희생하여 지각상 더 좋은 체험 효과를 얻는 것을 지원

  • SuspenseList: 한 조의 강등 효과의 출현 순서, 및 병존 수량을 제어하는 것을 지원

P.S.가장 간단한 강등 전략은 loading 이며,其它는 캐시 값을 사용,さら에는 광고를 한 토막, 미니 게임을 시작 등도 강등으로 간주합니다

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성