내가 Concurrent UI 패턴에 관해 알게 된 건 올해 6월 말에 '선언형 프로그래밍으로 이해하기 쉬운 코드 작성하기'라는 아티클을 읽은 후였다.
나는 지난 포스팅에서 자바스크립트에서 제공하는 메서드를 활용하여 선언형 코드로 리팩토링한 과정을 올렸다.
이번 포스팅에서는 Concurrent UI 패턴의 정의와 Concurrent UI 패턴을 활용하여 플랜 빙고 코드를 선언형 코드로 리팩토링한 과정을 보여주겠다.
Concurrent UI Pattern이란 도대체 뭘까?
설명하기 전에 먼저 이 패턴이 나오게 된 배경을 알아보자.
프론트엔드 개발할 때 우리는 '사용자 경험에 관한 고민'을 많이 한다.
요즘은 과거와 달리 PC, 모바일 기기, 다양한 IoT 디바이스를 통해 웹 페이지를 열람한다. 그러다보니 어떤 디바이스에서는 복잡하고 무거운 UI 변경이 쾌적하게 반영되지 못하는 경우가 있다. 또한 상대적으로 안 좋은 인터넷 환경에서는 API 응답이 지연되거나 누락되어서 화면이 정상적으로 노출되지 않는 경우도 생길 수 있다. 이는 모두 사용자 경험에 안 좋은 영향을 끼친다.
지금까지는 다양한 환경의 사용자에게 쾌적한 사용자 경험을 제공하기 위한 작업이 임기응변식으로 진행되는 경우가 많았다. React 팀은 이런 '임기응변식'으로 대응되는 사용자 경험 향상 요소들을 라이브러리단에서 제공할 수 있게끔 React 17에서는 Concurrent Mode를 제공했고, React 18에서 concurrent features를 채택했다.
리액트 개발 팀에서는 '우선순위에 따른 화면 렌더', '컴포넌트의 지연 렌더' 그리고 '로딩 화면의 유연한 구성' 등을 쉽게 구성할 수 있도록 특성화된 기능을 사용한 UI 개발 패턴을 Concurrent UI Pattern이라 부르고 있다.
Suspense
란
<Suspense>
lets you display a fallback until its children have finished loading.
Suspense를 사용하면 컴포넌트가 렌더링하기 전에 다른 작업이 먼저 이루어지도록 '대기합니다'.
React 18에서는 Suspense
를 Relay, Next.js, Hydrogen, Remix와 같은 프레임워크에서 data fetching을 하기 위해 쓸 수 있다.
기본 문법
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
나는 다음과 같이 <Suspense>
컴포넌트를 사용하여 '로딩 화면의 유연한 구성'을 구성하였다.
Suspense 적용 전 코드 일부
isLoading
의 값이 true
일 때 <LoadingSpinner />
를 화면에 보여주고, props.mainBoardData.id
의 값이 들어오면 isLoading
의 값이 false
로 바꿔 <CheckBoard/>
를 화면에 보여주는 코드이다.
이게 바로 앞서 언급한 리액트 팀에서 말한 '임기응변'식 대응이다.
Suspense 적용 후 코드 일부
Suspense
를 사용했더니 useEffect
, useState
, if문
을 사용할 필요가 없어져서 코드가 깔끔해졌다.
또한 Suspense
를 활용한 코드가 화면을 어떻게(HOW) 그릴지에 집중하는 것이 아니라 무엇을(WHAT) 보여줄 것인지에 집중한 것으로 보인다. 이것을 바로 선언형 코드라고 부른다.
useTransition
이란
useTransition
is a React Hook that lets you update the state without blocking the UI.
기본 문법
const [isPending, startTransition] = useTransition()
Parameters
useTransition
은 아무 파라미터를 가지지 않는다.
Returns
useTransition
은 다음 두 아이템이 든 배열을 반환한다.
1. isPending
flag는 pending transition인지 아닌지 알려준다.
2. startTransition
transition할 것임을 보여주는 함수이다.
useTransition
을 사용하는 방법은 아래 예시를 참고하자.
나는 useTransition
을 활용하여 버튼에 pending indicator를 만들었다.
단계별로 useTransition
을 사용하는 방법을 소개한 후 Button.js의 전체 코드를 보여주겠다.
1. useTransition
을 import한 후 useTransition()
을 불러줬다.
2. 버튼이 클릭되면 작동하는 onClickTransitionHandler
를 만들어줬다.
만약 부모 컴포넌트로부터 onClickHandler
의 값을 받았다면 startTransition()
이 작동된다.
startTransition()
을 통해 transition이 시작되면 isPending
의 값이 true
가 된다. ( transition이 종료되면 isPending
의 값은false
가 된다.)
3. <button>
에 onClickTransitionHandler
와 isPending
을 다음과 같이 넣었다.
onClick={onClickTransitionHandler}
, disabled={isPending}
그리고 <p>{isPending ? "로딩중..." : props.description}</p>
가 추가한 부분이다.
Button.js 전체 코드
import styles from "@/styles/style.module.css";
import btnStyles from "@/components/layout/Button.module.css";
import { useTransition } from "react";
function Button(props) {
const [isPending, startTransition] = useTransition();
let classColor;
if (props.btnColor === "btnNoneColor") {
classColor = "btnNoneColor";
} else {
classColor = "btnColor";
}
const onClickTransitionHandler = () => {
if (props.onClickHandler) {
startTransition(() => {
props.onClickHandler();
});
}
};
return (
<button
type={props.type}
className={[btnStyles.button, styles[classColor]].join(" ")}
onClick={onClickTransitionHandler}
disabled={isPending}
>
<p>{isPending ? "로딩중..." : props.description}</p>
</button>
);
}
export default Button;
이렇게 기존의 코드를 Concurrent UI Pattern을 적용하여 선언형 코드로 리팩토링했다. 이 과정을 통해 React 18의 새로운 기능을 사용하는 방법을 익혔으며, 코드를 더욱 깔끔하게 쓸 수 있었다.
다음에는 이 Concurrent UI 패턴을 활용하여 스켈레톤 컴포넌트를 만들 예정이다. 만든 후에 꼭 블로그에 포스팅하도록 약속하겠다.
참고 자료