• 프로그래머스, 타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 5기 강의에서 번외로 진행되는 활동에 대한 내용을 정리하는 포스팅.

  • React.js (19버전)을 주제로 삼아 진행되는 스터디.



2주차 담당 주제 : React.js 내장 컴포넌트

Fragment

  • Fragment 혹은 <>...</> 로 표기, 여러 JSX 노드를 함께 그룹화할 수 있다.

Profiler

  • React 트리의 렌더링 성능을 프로그래밍 방식으로 측정할 수 있다.

StrictMode

  • 초기에 버그를 찾는 데 도움이 되는 추가 개발 전용 검사를 사용할 수 있다.

Suspense

  • 자식 컴포넌트를 로딩하는 동안 Fallback을 표시할 수 있다.



1. Fragment

const App = () => {
  return (
    <h1>안녕하세요</h1>
    <p>React Fragment를 사용한 예제입니다.</p>
  )
}
  • React.js App을 하나 생성하고, 위와 같이 컴포넌트를 구성한 뒤에 실행시켜보면..

  • 위와 같은 에러가 발생한다.

Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?
JSX에서는 반환되는 요소가 반드시 하나의 부모 요소로 감싸져 있어야 한다.

  • React.js, JSX 문법에서는 하나의 컴포넌트에서 반환하는return 요소들은 반드시 하나의 부모 요소 아래 묶여있어야 한다.

  • 컴포넌트에 작성한 요소들은 JSX 내부적으로 React.createElement() 함수를 통해 변환되는데.. 하나의 컴포넌트에서 2개 이상의 createElement가 동작할 수는 없기 때문.

const App = () => {
  return React.createElement("h1", null, "안녕하세요"),
  return React.createElement("p", null, "React Fragment를 사용한 예제입니다."); 
}
  • 요런 이상한 형태가 되어버린다. 그래서 에러가 발생하는 것.



그렇다면..

const App = () => {
  return (
    <div>
      <h1>안녕하세요</h1>
      <p>React Fragment를 사용한 예제입니다.</p>
    </div>
  )
}
  • 가장 간단한 해결책은 div 태그 등을 이용해서 부모 요소를 만들어버리는 것.

  • 그런데.. 이렇게되면 몇 가지 문제점들이 발생한다.

  • div도 DOM의 일원이다. 전체 구조가 복잡해지면서 렌더링 성능에 저하를 일으킬 수 있고..

  • 부모-자식 관계와 연관되어있는 CSS 레이아웃 및 스타일링 구조가 꼬여버릴 수 있다.

  • 외부에서 컴포넌트의 구조를 분석하는 경우에도 쓸때없는 DOM이 하나 더해지는게 좋은 일은 아니고..



그래서 Fragment!

const App = () => {
  return (
    <Fragment key={id}>
      <h1>안녕하세요</h1>
      <p>React Fragment를 사용한 예제입니다.</p>
    </Fragment>
  )
}
  • Fragment는 React.js에 내장되어 있는 기본 컴포넌트의 일원이다.

  • 이 녀석의 특징은.. 실제 DOM 요소로 간주되지 않아 렌더링이 이루어지지 않는다는 것.
    - Virtual DOM에는 존재하지만, 실제 DOM에는 존재하지 않는다.
    - 그래서 DOM 변경 및 업데이트에 사용되지 않아 최적화 측면에서도 좋다.

  • 단지 JSX 요소들을 하나로 그룹화 하는 역할만 수행한다.

  • 그럼에도 속성 요소를 사용할 수 있어, 부모 요소가 key 속성을 보유해야할 때도 문제없이 사용가능하다.



축약하여 사용하는 것도 된다!

const App = () => {
  return (
    <>
      <h1>안녕하세요</h1>
      <p>React Fragment를 사용한 예제입니다.</p>
    </>
  )
}
  • Fragment라는 문자를 입력할 필요 없이, 아무 텍스트도 입력되지 않은 비어있는 태그로 축약 사용이 가능하다.

  • 이렇게 되면 부모 컴포넌트가 Fragment라는 점을 더 명확하게 드러낼 수 있어서 좋다.

  • 다만, 축약 사용시에는 속성 요소를 사용할 수 없다. key 속성 등을 보유해야하는 경우에는 축약 사용은 불가능..



번외 - 난 Fragment를 사용하기 싫어.

const App = () => {
  return [
    <h1>안녕하세요</h1>,
    <p>React Fragment를 사용한 예제입니다.</p>
  ];
}
  • 그렇다면 요렇게 배열 형태로 반환해주면 된다.

  • 다만 이러면 기존 JSX 문법의 반환 방식과 다른 방식을 사용하는 셈이라.. 그냥 Fragment를 쓰는게 더 좋다.



2. Profiler

  • React 애플리케이션의 성능을 측정하고 최적화하는 데 사용되는 내장 컴포넌트.

  • React.js의 최적화 기준 중 하나는.. 컴포넌트의 불필요한 리렌더링을 방지하는 것.

const MyComponent = () => {
  console.log("컴포넌트 렌더링됨!");
  return <div>안녕하세요!</div>;
};

const App = () => {
  return (
    <div>
      <MyComponent />
    </div>
  );
};
  • 위와 같은 구조의 App이 있다고 가정해보자.

  • 여기에서 MyComponent가 어떤 상황에서(마운트, 업데이트, 언마운트) / 렌더링에는 얼마의 시간이 걸렸고 / 렌더링이 언제 시작되었고 / 변경 사용이 적용된 시간은 언제이며 / 어떻게 렌더링이 발생하는지 등의 사항을 알 수 있으려면?

  • Profiler를 사용하면 된다.

import React, { Profiler } from "react";

const MyComponent = () => {
  return <div>안녕하세요!</div>;
};

const onRenderCallback = (
  id, // 프로파일링된 컴포넌트의 ID
  phase, // 'mount' (처음 마운트) 또는 'update' (업데이트)
  actualDuration, // 실제 렌더링 시간 (ms)
  baseDuration, // 최적화되지 않은 예상 렌더링 시간
  startTime, // 렌더링이 시작된 시간
  commitTime, // 렌더링 결과가 커밋된 시간
  interactions // 트리거된 상호작용
) => {
  console.log(`컴포넌트: ${id}`);
  console.log(`렌더링 단계: ${phase}`);
  console.log(`실제 렌더링 시간: ${actualDuration}ms`);
  console.log(`기본 렌더링 시간: ${baseDuration}ms`);
  console.log(`렌더링 시작 시간: ${startTime}`);
  console.log(`커밋된 시간: ${commitTime}`);
};

const App = () => {
  return (
    <Profiler id="MyComponent" onRender={onRenderCallback}>
      <MyComponent />
    </Profiler>
  );
};

export default App;
  • 위와 같이 Profiler를 적용하고, Profiler의 분석 결과를 콘솔에 출력하도록 콜백함수를 작성한 뒤.. App을 실행시켜보면..

  • 이렇게 분석 결과를 조회할 수 있다. Profiler에서 제공하는 분석 값은 아래와 같다.
매개변수설명
idProfiler가 감싸고 있는 컴포넌트의 식별자
phase'mount' 또는 'update' (마운트 또는 업데이트 단계)
actualDuration실제 렌더링에 걸린 시간 (최적화된 상태)
baseDuration기본 렌더링 시간 (최적화되지 않은 상태)
startTime렌더링이 시작된 시간 (React 내부 시간 기준)
commitTime변경 사항이 커밋된 시간
interactions렌더링을 유발한 상호작용 정보



사용방법 및 주의점

  • 컴포넌트 전체 혹은 부분에 모두 적용 가능하다.
<App>
  <Profiler id="Sidebar" onRender={onRender}>
    <Sidebar />
  </Profiler>
  <Profiler id="Content" onRender={onRender}>
    <Content />
  </Profiler>
</App>
  • 상대적으로 가벼운 녀석이긴 한데.. 그렇다고 마구 남발하면 메모리에 부담이 갈 수 있으니 주의.

  • 아 그리고 '개발모드'에서만 동작하는 녀석이다.



성능은 어떻게 최적화하지?

  • 기본적인 방법은 React.memo, useMemo 혹은 useCallback같은 메모이제이션 기법을 사용하는 건데..

  • 메모이제이션도 무조건 좋은 방법은 아니다. 요 내용은 이전 포스팅 참조. 최적화라는 개념이 좀 복잡하다..

  • 이미지 최적화 부분은 요 포스팅 참조.

  • 이외에는 대량의 데이터를 한 번에 받아오는것 혹은 한 번에 렌더링 하는 것을 방지하는거..

  • React.lazy와 Suspense를 사용하여 필요한 컴포넌트만 로드하도록 하는 거..

  • useState의 사용을 최소화하는 것.. 등등이 있다.
    - useState에서 제어하는 state가 변경되면 해당 컴포넌트 전체가 리렌더링 되기 때문.
    - useState가 여러 개면, useReducer로 통합해서 관리하던지..
    - useRef로 값을 관리할 수 있다면 그렇게 하는.. 방법 등이 있다.



useRef로 값을 관리?

  • useRef의 주된 용도는 DOM 요소의 제어. 따라서 useRef로 값을 관리한다는게 무슨 해괴한 소리인가 싶지만..
import { useRef, useState } from "react";

const RefCounter = () => {
  const countRef = useRef(0); // 리렌더링되지 않는 변수
  const [count, setCount] = useState(0); // 리렌더링되는 상태

  const handleRefClick = () => {
    countRef.current += 1;
    console.log("Ref 값:", countRef.current); // UI에는 반영되지 않음
  };

  const handleStateClick = () => {
    setCount(count + 1); // 상태 변경 → 리렌더링 발생
  };

  return (
    <div>
      <p>useRef 값: {countRef.current} (리렌더링되지 않음)</p>
      <p>useState 값: {count} (리렌더링됨)</p>
      <button onClick={handleRefClick}>useRef 증가</button>
      <button onClick={handleStateClick}>useState 증가</button>
    </div>
  );
};
  • current 속성을 이용해서 값을 제어하는데 사용할 수도 있다.

  • 이 경우에는 값이 변화해도, 리렌더링이 발생하지 않는다!

  • 따라서 값이 변경되었을 때, 리렌더링이 발생하지 않아도 되는 값은 useRef를 사용하는게 최적화 측면에서 좋다.



3. StrictMode

  • React.js로 개발을 하다가.. 중간에 테스트를 하기 위해 App을 실행시키면.

  • 무슨 이유에서인지 기능이 2번씩 동작하는 이상한 현상이 발생한다.

  • 원인을 구글링해보면 하나의 결론에 도달하게 된다.

StrictMode를 끄시면 됩니다.



StrictMode

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
  • React 애플리케이션에서 잠재적인 문제를 감지하고 경고하는 역할을 하는 내장 컴포넌트.

  • 개발 환경에서만 동작한다.

  • 개발자가 따로 설정해줄 필요 없이, CRA나 Vite로 App를 생성할 때 index 파일에서 StrictMode가 자동 적용되어있다.



그래서 얘가 뭘 감지해줌?

  • 우선 2025년 기준으로 더 이상, 공식적으로 사용을 권장하지 않는 레거시 코드의 사용 여부를 확인해준다.

  • 구형 클래스 컴포넌트에서 사용되던 componentWillMount, componentWillUpdate 등이 사용되었다면 경고를 출력해준다.
    - React 18 버전 이후에는 Auto Batching 및 병렬 렌더링 문제 때문에 레거시 메서드를 사용하면 안되기 때문.
    - 병렬 렌더링? usetransition 등을 통해 연산 우선순위를 나누어 작업 중요도에 따라 순서를 조정하는 방식.
    - Auto Batching? 여러 개의 상태 업데이트가 자동으로 하나의 렌더링(batch)으로 묶어 최적화하는 기법.

import React, { Component } from "react";
import ReactDOM from "react-dom";

class MyComponent extends Component {
  componentDidMount() {
    const node = ReactDOM.findDOMNode(this);
    console.log(node); // StrictMode에서 경고 발생!
  }

  render() {
    return <div>안녕하세요!</div>;
  }
}
  • React에서 DOM 요소를 찾을 때 사용되던 findDOMNode() 등이 사용되는 경우에도 경고.
    - useRef를 사용하는게 좋다.
class MyComponent extends React.Component {
  render() {
    return <div ref="myRef">안녕하세요!</div>;
  }
}
  • useRef를 사용하지 않고, DOM 요소에 직접 ref 속성을 사용하는 경우에도 경고.

  • 이중 렌더링을 통해 버그를 사전에 방지.
    - 같은 기능을 2번 실행시켜봤는데.. 결과가 다르다? 이거 이상하다!



useEffect의 예상치 못한 동작 감지

  • useEffect를 2번 실행시켜보고, 문제가 없으면 OK.

  • 그렇지 않으면? 의존성 배열을 고치던지 cleanup 함수를 쓰게하던지 한다
    - cleanup 함수? useEffect에서 '컴포넌트가 unmount 될 때 실행되는 부분'.



주의점

  • 요 녀석도 Profiler처럼 부분적으로 적용할 수 있다.
import { StrictMode } from 'react';

function App() {
  return (
    <>
      <Header />
      <StrictMode>
        <main>
          <Sidebar />
          <Content />
        </main>
      </StrictMode>
      <Footer />
    </>
  );
}



4. Suspense

  • 비동기 컴포넌트 로딩 중 "로딩 상태(Loading State)"를 쉽게 관리할 수 있도록 도와주는 내장 컴포넌트.
import { useState, useEffect } from "react";

const MyComponent = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((res) => res.json())
      .then((result) => {
        setData(result);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>로딩 중...</p>;

  return <div>{data.title}</div>;
};
  • useState()와 useEffect()를 활용한 비동기 데이터 패칭에서 로딩 상태를 관리하는 예제.

  • 그런데.. Suspense를 사용하면 요걸 더 간단하게 처리할 수 있다.



import React, { Suspense, lazy } from "react";

// ❗️ 동적으로 import하여 컴포넌트 로딩
const LazyComponent = lazy(() => import("./LazyComponent"));

const App = () => {
  return (
    <Suspense fallback={<p>로딩 중...</p>}>
      <LazyComponent />
    </Suspense>
  );
};

export default App;
  • 데이터가 준비되기 전까지, fallback UI를 표시.

  • 데이터가 로드되면, 컴포넌트가 다시 렌더링되면서 정상적인 UI를 표시하는 원리.



왜 Suspense를 사용해야하는가?

  • useState & useEffect을 섞어서 로딩 상태를 관리할 필요가 없다. Suspense가 알아서 해준다.

  • 코드 분할Code Splitting을 지원해준다. lazy()의 사용을 통해, 특정 컴포넌트는 필요할 때에만 '분할'해서 로드하도록 구성한다.
    - 초기 로딩에 필요한 용량이 줄어든다는 뜻.

  • 비동기 데이터 패칭도 지원한다. 패칭 함수에서 promise만 반환시켜준 뒤, Suspense에 사용하면 알아서 처리해준다.

  • 로딩 UI 제어도 간편하다. 그냥 fallback 속성에 로딩 UI를 넣어주면 Suspense가 알아서 적절히 처리해준다.

주의점

  • 얘가 만능은 아니다.

  • 일단 데이터 패칭 방식이 살짝 달라지기 때문에.. 기존 패칭 방식이 적용된 코드에 Suspense를 쓰기에는 리팩토링 규모가 부담스러워질 수 있다.

  • SSR에서는 React Server Components를 섞어서 사용해야 한다?



Suspense를 적용한 코드 예시

import React, { Suspense } from "react";

// ✅ 1. 비동기 데이터를 가져오는 함수
const fetchData = () => {
  let data;
  let promise = fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then((res) => res.json())
    .then((result) => {
      data = result;
    });

  // 🚀 Suspense가 기다릴 수 있도록 Promise를 던짐
  throw promise;
};

// ✅ 2. Suspense 내부에서 데이터를 불러오는 컴포넌트
const MyComponent = () => {
  const data = fetchData();
  return <div>{data.title}</div>;
};

// ✅ 3. Suspense로 감싸서 로딩 상태 관리
const App = () => {
  return (
    <Suspense fallback={<p>로딩 중...</p>}>
      <MyComponent />
    </Suspense>
  );
};

export default App;



fallback?

  • 로딩 중일 때 보여줄 UI.

  • 그냥 '로딩 중..'이라는 텍스트를 띄우는건 너무 멋이 없다. 성의도 없고..

  • 로딩 스피너 혹은 스켈레톤 UI 등을 컴포넌트로 만들어서 사용하면 된다.

혹은..

  • 실제 콘텐츠가 로드되기 전에 콘텐츠의 레이아웃을 미리 보여주는 UI.

  • 실제 데이터가 로드되었을 때의 레이아웃과 유사한 구조를 미리 표시하는 역할.



useTransition()과의 결합

  • 만약에 데이터를 불러와서 UI를 렌더링 하는 동안에.. UI가 잠시라도 멈추는 문제가 발생한다면?

  • useTransition()를 사용해볼 수 있다.

import { useState, useTransition, Suspense } from "react";

// ❗️ 비동기 데이터를 가져오는 컴포넌트
const DataComponent = React.lazy(() => import("./DataComponent"));

const App = () => {
  const [isPending, startTransition] = useTransition();
  const [show, setShow] = useState(false);

  const handleClick = () => {
    startTransition(() => {
      setShow(true);
    });
  };

  return (
    <div>
      <button onClick={handleClick}>데이터 로드</button>
      {isPending && <p>로딩 중...</p>}
      {show && (
        <Suspense fallback={<p>데이터 로딩 중...</p>}>
          <DataComponent />
        </Suspense>
      )}
    </div>
  );
};
  • 버튼을 클릭하면 handleClick()이 실행됨.

  • startTransition()을 사용하여 UI가 즉시 반응하도록 하고, 무거운 작업(setShow(true))을 비동기적으로 실행.

  • show 상태가 true가 되면서 Suspense 내부의 DataComponent가 마운트됨.

  • DataComponent는 lazy()로 불러오므로 비동기적으로 로딩됨.

  • 로딩 중일 때 Suspense fallback이 표시됨.

  • 로딩이 완료되면 DataComponent가 정상적으로 렌더링됨.

useTransition()의 역할

  • 상태 업데이트를 비동기적으로 실행.

  • startTransition() 내부의 상태 업데이트는 React가 우선순위를 낮게 두고 처리.

  • 작업량을 조절하여 UI 지연 없이 동작이 원활하게 작동되도록 한다.

Suspense는 뭘 하지?

  • DataComponent는 lazy-loaded(지연 로드)된 컴포넌트이므로 처음에는 즉시 렌더링되지 않는다.

  • show 상태가 true가 되면 DataComponent가 마운트되도록.

  • 로딩이 완료될 때까지 Suspense의 fallback UI가..

  • 로딩이 끝나면 DataComponent가 최종적으로 화면에 표시되도록 한다.

profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글