[React 19] 공식문서 톺아보기 - React 컴포넌트

Study• 수십명이 읽음
리액트에서 사용하는 Fragment, Profiler, StrictMode, Suspense에 대해 공부하였습니다.

Fragment

  • <Fragment>는 종종 <>...</> 구문으로 사용하고, 래퍼 노드 없이 엘리먼트를 그룹화할 수 있다.

가상 DOM 비교

{
  show ? (
    <>
      <Counter />
    </> // Fragment 사용
  ) : (
    <Counter /> // 직접 렌더링
  );
}
{
  show ? (
    <>
      <Counter />
    </> // Fragment 사용
  ) : (
    <Counter /> // 직접 렌더링
  );
}
  • 같은 컴포넌트라고 생각하여 state를 초기화하지 않는다.
  • Fragment나 배열로 감싸더라도 한 단계 깊이까지는 동일하다고 판단한다.
{
  show ? (
    <>
      <>
        <Counter />
      </>
    </> // 중첩 Fragment
  ) : (
    <Counter />
  );
}
{
  show ? (
    <>
      <>
        <Counter />
      </>
    </> // 중첩 Fragment
  ) : (
    <Counter />
  );
}
  • 두 단계 깊이 차이의 경우 state를 초기화한다.

여러 엘리먼트 반환하기

<>
  <PostTitle />
  <PostBody />
</>
<>
  <PostTitle />
  <PostBody />
</>
  • JSX에서는 하나의 엘리먼트를 반환해야하므로 Fragment로 묶어 반환한다.
  • 레이아웃이나 스타일에 영향을 주지 않는다.

리스트 렌더링

  return posts.map(post =>
    <Fragment key={post.id}>
      <PostTitle title={post.title} />
      <PostBody body={post.body} />
    </Fragment>
  );
  return posts.map(post =>
    <Fragment key={post.id}>
      <PostTitle title={post.title} />
      <PostBody body={post.body} />
    </Fragment>
  );
  • key값을 할당해야할때 위와같이 <></> 문법을 사용하는 대신 명시적으로 Fragment를 작성해야 한다.

Profiler

  • <Profiler>를 통해 React 트리의 렌더링 성능을 프로그래밍 방식으로 측정할 수 있다.
  • 프로파일링은 추가적인 오버헤드를 더하기 때문에, 프로덕션에서는 기본적으로 비활성화 되어있다.

사용예시

<Profiler id='App' onRender={onRender}>
  <App />
</Profiler>
<Profiler id='App' onRender={onRender}>
  <App />
</Profiler>
  • id: 성능을 측정하는 UI 컴포넌트를 식별하기 위한 문자열
  • onRender: 프로파일링된 트리 내의 컴포넌트가 업데이트될 때마다 React가 호출한다.
    • 렌더링된 내용과 소요된 시간에 대한 정보를 받는다.

onRender

function onRender(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
) {
  // 렌더링 시간 집계 혹은 로그...
}
function onRender(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
) {
  // 렌더링 시간 집계 혹은 로그...
}
  • id: 커밋된 <Profiler> 트리의 문자열 id 프로퍼티
    • 프로파일러를 다중으로 사용하고 있는 트리 내에서 어떤 부분이 커밋 되었는지 식별할 수 있게 해준다.
  • phase"mount""update" 혹은 "nested-update". 트리가 최초로 마운트되었는지 또는 Props, State, Hook의 변경으로 인해 리렌더링 되었는지 알 수 있다.
  • actualDuration: 현재 업데이트에 대해 <Profiler>와 자식들을 렌더링하는데 소요된 시간
  • baseDuration: 최적화 없이 전체 <Profiler> 하위 트리에 대해 걸리는 시간, 이 값은 최악의 렌더링 비용(예: 최초 마운트 또는 메모이제이션이 없는 트리)을 추정한다.
  • startTime: React가 현재 업데이트 렌더링을 시작한 시점
  • commitTime: React가 현재 업데이트를 커밋한 시점

StrictMode

  • <StrictMode>를 통해 개발 중에 컴포넌트에서 일반적인 버그를 빠르게 찾을 수 있도록 합니다.
  • 개발환경에서만 실행되며, 컴포넌트가 순수한지, Effect 클린업 함수가 필요한지 찾을 수 있다.
  • Strict Mode는 실수로 작성된 순수하지 않은 코드를 찾아내기 위해 몇 가지 함수(순수 함수여야 하는 것만)를 개발 환경에서 두 번 호출한다.
    • 컴포넌트 함수 본문 (단, 최상위 로직만 해당하며, 이벤트 핸들러 내부의 코드는 포함하지 않는다.)
    • useStateset 함수, useMemo, 또는 useReducer에 전달한 함수
    • constructorrendershouldComponentUpdate와 같은 일부 클래스 컴포넌트 메소드
    • Strict Mode가 켜져 있으면 React는 모든 Effect에 대해 개발 환경에서 한 번 더 셋업+클린업 사이클을 실행한다.

ref 콜백 다시 실행

ref={(node) => {
  const list = itemsRef.current;
  const item = {animal: animal, node};
  list.push(item);
  console.log(`✅ Adding animal to the map. Total animals: ${list.length}`);
  if (list.length > 10) {
    console.log('❌ Too many animals in the list!');
  }
  return () => {
    // 🚩 No cleanup, this is a bug!
  }
}}
ref={(node) => {
  const list = itemsRef.current;
  const item = {animal: animal, node};
  list.push(item);
  console.log(`✅ Adding animal to the map. Total animals: ${list.length}`);
  if (list.length > 10) {
    console.log('❌ Too many animals in the list!');
  }
  return () => {
    // 🚩 No cleanup, this is a bug!
  }
}}
  • 엄격모드는 개발단계에서 잠재적인 문제를 감지하기 위해 컴포넌트를 의도적으로 두 번 렌더링하는데 이 과정에서 ref 콜백도 두번 실행된다.
    • 첫 번째 렌더링: ref 콜백이 null로 호출된다. (기존 참조 정리)
    • 두 번째 렌더링: 실제 DOM 요소나 컴포넌트 인스턴스로 호출된다.
  • ref 콜백에서 다음과 같은 코드를 작성한다면 필수로 클린업 함수를 작성해야한다.
    • 이벤트 리스너 추가
    • 외부 라이브러리 인스턴스 생성
    • 타이머/구독(subscription) 관리
  • useEffect와의 차이점
    • ref 콜백의 클린업은 DOM 요소의 연결/해제시 실행되지만,
    • useEffect의 클린업은 의존성 배열이 변경되거나 언마운트 시만 실행된다.

Suspense

  • <Suspense>는 자식 요소를 로드하기 전까지 화면에 대체 UI Fallback를 보여준다.
<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>
<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>
  • fallback: 실제 UI가 로딩되기 전까지 대신 렌더링되는 대체 UI
    • Suspense는 children의 렌더링이 지연되면 자동으로 fallback으로 전환하고, 데이터가 준비되면 children으로 다시 전환한다.
    • 만약 fallback의 렌더링도 지연되면, 가장 가까운 부모 Suspense가 활성화된다.

Suspense 도중 생성된 state는 유지되지 않는다

function MyComponent() {
  const [count, setCount] = useState(0);

  const data = use(fetchData()); // 여기서 suspend

  return (
    <div>
      {data} / Count: {count}
    </div>
  );
}
function MyComponent() {
  const [count, setCount] = useState(0);

  const data = use(fetchData()); // 여기서 suspend

  return (
    <div>
      {data} / Count: {count}
    </div>
  );
}

React는 컴포넌트가 처음으로 마운트 되기 전에, 지연된 렌더링을 하는 동안 어떤 State도 유지하지 않습니다. 컴포넌트가 로딩되면 React는 일시 중지된 트리를 처음부터 다시 렌더링합니다.

  • React가 컴포넌트를 렌더링하다가 중간에 fetchData를 만나면 일시중단한다.
    • 중단하는 동안 fallback ui가 표시된다.
    • 컴포넌트 실행중에 useState(0)도 실행 도중에 멈추고 count는 메모리에 저장되지 않는다.
  • 데이터 fetch가 끝나고 다시 렌더링되면 useState(0)부터 다시 시작한다.

useLayoutEffect와 Suspense

useLayoutEffect(() => {
  console.log('레이아웃 측정 실행');
  return () => console.log('레이아웃 측정 정리'); // ✅ 로딩 중일 때 호출됨
}, []);
useLayoutEffect(() => {
  console.log('레이아웃 측정 실행');
  return () => console.log('레이아웃 측정 정리'); // ✅ 로딩 중일 때 호출됨
}, []);
  • 컴포넌트가 로딩중이면 클린업함수를 실행한다.
    • 로딩중인 컴포넌트는 숨겨진 상태로 DOM요소에 접근할 수 없다.
  • 로딩 완료후 다시 실행한다.

Suspense를 활성화하는 조건

  • Relay와 Next.js 같이 Suspense가 가능한 프레임워크를 사용한다.
  • lazy를 활용한 지연 로딩 컴포넌트
    • 컴포넌트가 아직 로드되지 않았을 때 렌더링을 일시 중단(suspend)한다.
  • use를 사용해서 캐시된 Promise 값 읽기
    • React는 컴포넌트 렌더링 중 use(somePromise)를 만나면 Suspense 상태로 전환한다.
    • Suspense는 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터를 감지하지 않는다.

로딩순서 만들기

  • Suspense로 감싸진 구성요소중 하나라도 지연되면 로딩이 표시되고 모두 보일 준비가 되면 함께 보여진다.
    • 따라서 오래걸리는 컴포넌트를 함께 Suspense로 묶게되면 다함께 느려지므로 Suspense를 세분화한다.
  • 여러 Suspense 컴포넌트를 중첩하여 로딩 순서를 만들 수 있다.
    • Suspense를 중첩하여 먼저 준비가 된 컴포넌트부터 보여줄 수 있다.
    • 먼저 보여줄 컴포넌트 경계를 Suspense를 통해서 지정할 수 있다.

useDeferredValue와 Suspense

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
  <>
    <input value={query} onChange={(e) => setQuery(e.target.value)} />

    <Suspense fallback={<h2>Loading...</h2>}>
      <SearchResults query={deferredQuery} />
    </Suspense>
  </>
);
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
  <>
    <input value={query} onChange={(e) => setQuery(e.target.value)} />

    <Suspense fallback={<h2>Loading...</h2>}>
      <SearchResults query={deferredQuery} />
    </Suspense>
  </>
);
  • 함께 사용할 경우 새 결과가 준비될때까지 이전 결과를 보여줄 수 있다.
  • 사용자의 빠른 입력에 입력마다 데이터요청을 하는게 아니라 완성된 입력상태에 대해서 요청이 되고 그것에 대한 fallback을 보여준다.

startTransition와 Suspense

const handleTabChange = (newTab) => {
  startTransition(() => {
    setTab(newTab); // 탭 전환 지연 (중요하지 않은 업데이트)
  });
};
// ...
<Suspense fallback={<div>Loading...</div>}>
  {tab === 'home' ? <HomeTab /> : <ProfileTab />}
</Suspense>;
const handleTabChange = (newTab) => {
  startTransition(() => {
    setTab(newTab); // 탭 전환 지연 (중요하지 않은 업데이트)
  });
};
// ...
<Suspense fallback={<div>Loading...</div>}>
  {tab === 'home' ? <HomeTab /> : <ProfileTab />}
</Suspense>;
  1. 사용자가 탭을 클릭 → startTransition으로 상태 업데이트 지연.
  2. 새 탭의 컴포넌트(<ProfileTab />)가 로딩되면 Suspense가 fallback UI 표시.
  3. 로딩 완료 후 실제 컨텐츠 렌더링.
  • startTransition는 낮은 우선순위를 갖게해줘서 다른 급한 동작을 중간에 수행할 수 있게 해준다.
  • Suspense는 로딩중이라는 것을 시각적으로 보여준다.
  • 두개를 같이 쓰게되면 사용자는 로딩상태를 인지하면서도 화면이 멈추지 않고 다른 작업을 계속할 수 있다.
    • Suspense만 쓰게 되면 사용자는 다른 작업을 할수 없고 로딩이 길면 안좋은 UX가 된다.
    • startTransition만 쓰게 되면 로딩표시가 없어 반응이 없다고 느낄 수 있다.
    • 무조건 두개를 같이 쓴다고 위와같은 효과가 나타나는게 아니라 Suspense는 특정 조건을 만족해야 fallback UI를 보여준다.

주의사항

  • Suspense는 Promise를 throw하는 비동기 함수와만 작동한다.
  • Suspense는 SSR에서 스트리밍 렌더링과 함께 사용될 수 있지만,
  • startTransition은 클라이언트 사이드에서만 유효하다.

SSR과 Suspense

  • 스트리밍 서버 렌더링 API 중 하나(또는 프레임워크)를 사용하는 경우, React는 서버의 에러를 처리하기 위해 <Suspense>를 사용하기도 한다.
  • 컴포넌트가 서버에서 에러를 발생시키더라도 리액트는 렌더링을 중단하지 않고, 가장 가까운 <Suspense>의 fallback UI를 HTML에 넣어 보낸다.
  • 그리고 클라이언트에서 재시도를 해서 성공하면 정상적으로 표시하고 실패하면 에러바운더리가 에러 ui 표시한다.
    • 이러한 과정을 통해 서버에서의 에러로 인해 전체 페이지가 흰 화면이 되지 않도록 방지하고 클라이언트에서 복구할 기회를 준다.
    • 또한 이를 사용하여 일부 컴포넌트를 의도적으로 서버에서 렌더링하지 않도록 선택할 수 있다.
    if (typeof window === 'undefined') {
      throw Error('Chat should only render on the client.');
    }
    if (typeof window === 'undefined') {
      throw Error('Chat should only render on the client.');
    }

참고자료

읽어주셔서 감사합니다