[React 19] 공식문서 톺아보기 - React API

Study
리액트 api중에서 act, cache, lazy, startTransition, use 등에 대해서 공부하였습니다.

act

await act(async actFn)
await act(async actFn)
  • React 컴포넌트의 테스트 환경에서 비동기 업데이트(상태 변경, 효과 실행 등)가 올바르게 처리되도록 보장하는 유틸리티 함수이다.
  • 주로 Jest + React Testing Library 환경에서 컴포넌트 테스트시 사용된다.

테스트를 할때 act를 써야하는 이유

test('버튼 클릭 시 카운트 증가', () => {
  render(<Counter />);
  const button = screen.getByText('증가');

  button.click(); // ❌ act() 없음

  // 상태 업데이트가 완료되지 않았을 수 있음
  expect(screen.getByText('1')).toBeInTheDocument(); // 실패 가능성
});
test('버튼 클릭 시 카운트 증가', () => {
  render(<Counter />);
  const button = screen.getByText('증가');

  button.click(); // ❌ act() 없음

  // 상태 업데이트가 완료되지 않았을 수 있음
  expect(screen.getByText('1')).toBeInTheDocument(); // 실패 가능성
});
  • act()가 없으므로, React가 실제로 DOM을 업데이트하기 전에 검증 코드인 expect()가 실행될 가능성이 있다.

    • 업데이트가 우연히 빠르게 완료되면 통과할 수 있지만 안정적이지 않다.
  • act()는 테스트에서 React의 비동기 업데이트(상태변경, 렌더링 등)가 모두 완료될때까지 기다린뒤 검증할 수 있도록 하여 테스트의 안정성을 보장한다.

act를 직접사용하는 경우

  • 기본적으로 RTL(React Testing Library)를 사용하게 되면 직접 쓸 필요가 없다.
  • 하지만 저수준(React DOM Test Utils 등)으로 테스트할 때는 React가 상태 업데이트를 추적하지 못하므로 필요하다.
  • React 17 이하에서는 자동 배치(Automatic Batching)가 없어 act()로 수동 보정 필요하다.
  • 커스텀 테스트 환경 구축시 (테스트 라이브러리를 사용하지 않고), 테스트 라이브러리를 사용하지 않는 순수 환경에서 컴포넌트를 테스트할 때 필요하다.

cache

const cachedFn = cache(fn);
const cachedFn = cache(fn);
  • cache를 통해 가져온 데이터나 연산의 결과를 캐싱한다. (데이터 요청에 한정짓지 않는다)
  • React 서버 컴포넌트와 함께 사용한다.
  • cachefn의 캐싱된 버전을 반환하는데 이 과정에서 fn을 호출하지는 않는다.

같은 함수를 cache로 여러번 호출하지 않기

import { cache } from 'react';

import { calculateWeekReport } from './report';

export default cache(calculateWeekReport);
import { cache } from 'react';

import { calculateWeekReport } from './report';

export default cache(calculateWeekReport);
  • cache를 호출할 때마다 새 함수가 생성된다. 즉, 동일한 함수로 cache를 여러 번 호출하면 동일한 캐시를 공유하지 않는 다른 메모화된 함수가 반환된다.
  • 따라서 캐시함수를 선언할때는 컴포넌트 외부나 별도 파일에 선언하여 같은 메모화된 함수를 호출해야 한다.
// facebook/react/packages/react/src/ReactCacheClient.js
export const cache: typeof noopCache = disableClientCache
  ? noopCache
  : cacheImpl;
// facebook/react/packages/react/src/ReactCacheClient.js
export const cache: typeof noopCache = disableClientCache
  ? noopCache
  : cacheImpl;
  • cache로 감싼 함수를 클라이언트에서 사용했을때는 특별히 캐시기능이 존재하지는 않는다.

캐시 매커니즘

// facebook/react/packages/react/src/ReactCacheImpl.js
type CacheNode<T> = {
  s: 0 | 1 | 2; // 상태 (UNTERMINATED, TERMINATED, ERRORED)
  v: T | mixed; // 캐시된 값 또는 에러
  o: WeakMap<Function | Object, CacheNode<T>>; // 객체 캐시
  p: Map<Primitive, CacheNode<T>>; // 원시값 캐시
};
// facebook/react/packages/react/src/ReactCacheImpl.js
type CacheNode<T> = {
  s: 0 | 1 | 2; // 상태 (UNTERMINATED, TERMINATED, ERRORED)
  v: T | mixed; // 캐시된 값 또는 에러
  o: WeakMap<Function | Object, CacheNode<T>>; // 객체 캐시
  p: Map<Primitive, CacheNode<T>>; // 원시값 캐시
};
  • cache는 객체나 함수의 캐시, 원시값 캐시를 구분한다.
  • 리액트 오픈소스 코드에 의하면 캐시는 노드의 형태를 띄고있고 위와같은 구조를 가지고 있다.
for (let i = 0; i < arguments.length; i++) {
  const arg = arguments[i];
  if (typeof arg === 'function' || (typeof arg === 'object' && arg !== null)) {
    // 객체/함수 → WeakMap으로 참조 비교
    cacheNode = cacheNode.o?.get(arg) || createCacheNode();
    cacheNode.o.set(arg, cacheNode);
  } else {
    // 원시값 → Map으로 값 비교
    cacheNode = cacheNode.p?.get(arg) || createCacheNode();
    cacheNode.p.set(arg, cacheNode);
  }
}
for (let i = 0; i < arguments.length; i++) {
  const arg = arguments[i];
  if (typeof arg === 'function' || (typeof arg === 'object' && arg !== null)) {
    // 객체/함수 → WeakMap으로 참조 비교
    cacheNode = cacheNode.o?.get(arg) || createCacheNode();
    cacheNode.o.set(arg, cacheNode);
  } else {
    // 원시값 → Map으로 값 비교
    cacheNode = cacheNode.p?.get(arg) || createCacheNode();
    cacheNode.p.set(arg, cacheNode);
  }
}
  • 인자로 객체가 들어오게 되면 위와 같은 코드로 캐시 데이터가 있는지 확인하고 없으면 새롭게 생성하게 된다.

React는 서버 요청마다 모든 메모화된 함수들을 위해 캐시를 무효화한다.

  • 서버 요청이 발생할 때마다 (예: 페이지 새로고침, SSR 렌더링), React는 이전에 메모이제이션된 함수/값의 캐시를 초기화한다.

요청마다 캐시를 무효화하면 캐싱한 의미가 없지 않나?

서버캐시와 클라이언트 캐시의 차이를 알면 무효화하는게 당연하다.

서버 캐시와 클라이언트 캐시

  • 클라이언트 캐시는 컴포넌트 생명주기 동안 유지되지만 서버캐시는 단일 http 요청동안 유지된다.
  • 서버 컴포넌트로 한정짓는 이유도 단일 http 요청동안 유지되기 때문이다.
    • 클라이언트 컴포넌트는 렌더링 시기가 다르고 요청 시기도 달라 캐시가 될 경우, 예전 데이터가 표시되는 등 여러 문제가 발생할 수 있다.
항목서버 캐시클라이언트 캐시
위치서버 (예: CDN, 서버 메모리, DB 결과 등)브라우저 메모리, localStorage, React의 상태 등
대상모든 사용자에게 공통적으로 제공될 수 있음사용자 개별적으로 저장됨
지속성서버 설정에 따라 오래 유지되거나 무효화 정책이 있음페이지 이동 시 사라지거나, 탭 닫으면 삭제됨
예시Next.js의 fetch(..., { cache: 'force-cache' }), 서버에서 응답 저장React의 useMemo, 브라우저 캐시, useQuery의 클라이언트 캐시 등
사용 목적서버 부하 감소, 빠른 응답 제공렌더링 최적화, 사용자 경험 향상

사전에 데이터 받아두기

const getUser = cache(async (id) => {
  return await db.user.query(id);
});

function Page({ id }) {
  getUser(id);
  // ...
  return <Profile id={id} />;
}

async function Profile({ id }) {
  const user = await getUser(id);
  // ...
}
const getUser = cache(async (id) => {
  return await db.user.query(id);
});

function Page({ id }) {
  getUser(id);
  // ...
  return <Profile id={id} />;
}

async function Profile({ id }) {
  const user = await getUser(id);
  // ...
}
  • Page에서 실제 사용하지는 않지만 자식컴포넌트에서 사용하므로 미리 요청하여 데이터를 캐싱해둔다.
async function fetchData() {
  return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
  getData();
  // ... some computational work
  await getData();
  // ...
}
async function fetchData() {
  return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
  getData();
  // ... some computational work
  await getData();
  // ...
}
  • 첫번째 호출은 기다리지 않지만 두번째 호출은 기다린다.
  • 이 최적화는 데이터 불러오기를 기다리는 동안 React가 계산 작업을 계속할 수 있게 해 두 번째 호출에 대한 대기 시간을 줄일 수 있게 합니다.

createContext

const SomeContext = createContext(defaultValue);
const SomeContext = createContext(defaultValue);
  • createContext를 사용하면 컴포넌트가 Context를 제공하거나 읽을 수 있다.

SomeContext.Provider

const [theme, setTheme] = useState('light');
// Provider
return (
  <ThemeContext.Provider value={theme}>
    <Page />
  </ThemeContext.Provider>
);
// 이렇게도 사용이 가능하다.
return (
  <ThemeContext value={theme}>
    <Page />
  </ThemeContext>
);
const [theme, setTheme] = useState('light');
// Provider
return (
  <ThemeContext.Provider value={theme}>
    <Page />
  </ThemeContext.Provider>
);
// 이렇게도 사용이 가능하다.
return (
  <ThemeContext value={theme}>
    <Page />
  </ThemeContext>
);
  • 컴포넌트를 SomeContext.Provider로 감싸서 이 컨텍스트의 값을 모든 내부 컴포넌트에 제공한다.

SomeContext.Consumer - 이전 방식

// ⚠️ 이전 방식 (권장하지 않음)
<ThemeContext.Consumer>
  {(theme) => <button className={theme} />}
</ThemeContext.Consumer>
// ⚠️ 이전 방식 (권장하지 않음)
<ThemeContext.Consumer>
  {(theme) => <button className={theme} />}
</ThemeContext.Consumer>
  • 옛날 방식이긴하나 훅을 사용하지 않으니 서버컴포넌트에서도 사용이 가능하다.
    • 서버컴포넌트에서만 context를 쓰고 리렌더링이 필요없다면 변수를 통해서 context를 쓰는게 가능은 하다.
    • React가 감지할 수 없는 부분이기 때문에 서버컴포넌트에서 사용하는게 정상적이지는 않다.

useContext - 최근 방식

// ✅ 권장하는 방법
const theme = useContext(ThemeContext);
return <button className={theme} />;
// ✅ 권장하는 방법
const theme = useContext(ThemeContext);
return <button className={theme} />;

lazy

const SomeComponent = lazy(load);
const SomeComponent = lazy(load);
  • lazy를 사용하면 컴포넌트가 처음 렌더링될 때까지 해당 컴포넌트의 코드를 로딩하는 것을 지연할 수 있다.

    • 여기서 지연로드는 늦게 로드된다는 의미보다도 메인 번들에서 분리되는 개념이다.
    • 원래 일반적으로 선언된 컴포넌트는 사용여부와 관계없이 로드되지만 lazy로 감싼경우는 렌더링이 시도되기 전까지는 아예 해당 JS 코드는 브라우저에 존재하지 않게 된다.

동작 예시

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

// ...

<div>
  <button onClick={() => setShow(true)}>Show</button>
  {show && (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent /> {/* 👈 이 시점에서야 비로소 로드 시작! */}
    </Suspense>
  )}
</div>;
const LazyComponent = React.lazy(() => import('./LazyComponent'));

// ...

<div>
  <button onClick={() => setShow(true)}>Show</button>
  {show && (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent /> {/* 👈 이 시점에서야 비로소 로드 시작! */}
    </Suspense>
  )}
</div>;
  1. 버튼을 클릭하면 LazyComponent의 코드를 받아오기 시작한다.

  2. load는 Promise를 반환한다.

() => import('./MyComponent'); // → Promise<{ default: React.ComponentType }>
() => import('./MyComponent'); // → Promise<{ default: React.ComponentType }>
  1. React는 이 Promise가 resolve될 때까지 기다리면서 fallback ui를 보여준다.
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<div>Loading...</div>}>
  1. Promise가 해결되면, 해당 모듈의 default 속성을 가져온다.
  • 따라서 컴포넌트가 export default로 내보낸 경우가 아니라면, React.lazy를 사용할 수 없다.
  • 만약 Promise가 거부되면, React는 거부 이유를 가장 가까운 Error Boundary가 처리할 수 있도록 throw 한다.

컴포넌트 내부에 선언하지 말것

function Editor() {
  // 🔴 잘못된 방법: 이렇게 하면 다시 렌더링할 때 모든 상태가 재설정됩니다.
  const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
  // ...
}

// ✅ 올바른 방법: `lazy` 컴포넌트를 컴포넌트 외부에 선언합니다.
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));

function Editor() {
  // ...
}
function Editor() {
  // 🔴 잘못된 방법: 이렇게 하면 다시 렌더링할 때 모든 상태가 재설정됩니다.
  const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
  // ...
}

// ✅ 올바른 방법: `lazy` 컴포넌트를 컴포넌트 외부에 선언합니다.
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));

function Editor() {
  // ...
}
  • 리렌더링이 발생할때마다 새로운 lazy 컴포넌트가 생성되며 import()가 재실행되고 동일한 청크 파일을 반복 요청된다.
  • 경로가 동적으로 변해야할 경우는 useMemo로 최적화한다.
    const LazyComponent = useMemo(
      () => React.lazy(() => import(`./${componentPath}`)),
      [componentPath], // 경로 변경 시에만 재생성
    );
    const LazyComponent = useMemo(
      () => React.lazy(() => import(`./${componentPath}`)),
      [componentPath], // 경로 변경 시에만 재생성
    );

next dynamic vs react lazy

  • react lazy
    • Suspense 컴포넌트와 함께 사용해야 한다.
    • 클라이언트 사이드에서만 동작한다.
  • next dynamic
    • Next.js 전용 기능이다.
    • Suspense대신 loading prop을 통해 로딩 상태 처리가 가능하다.
    • next dynamic의 ssr 옵션 { ssr: true }을 지원한다.
      • 서버에서 컴포넌트를 불러와서 SSR HTML에 포함시킨다.
      • 클라이언트에서 별도 청크로 분리되어 초기 번들에는 포함되지 않는다.

memo

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
  • 컴포넌트를 memo로 감싸면 해당 컴포넌트의 메모된 버전을 얻을 수 있다.
  • 메모된 버전의 컴포넌트는 일반적으로 부모 컴포넌트가 리렌더링 되어도 Props가 변경되지 않았다면 리렌더링되지 않습니다.
  • arePropsEqual : 컴포넌트의 이전 Props와 새로운 Props를 비교하는 로직을 커스텀하게 구성할 수 있는 함수로 일반적으로 이 함수를 지정하지 않는다.
    • React는 기본적으로 Object.is로 각 Props를 비교한다.

memo를 추가해야하는 상황

  • 대부분의 상호작용이 투박한 앱의 경우(페이지 또는 전체 섹션 교체 등) 일반적으로 메모이제이션은 불필요하다.
  • memo로 최적화하는 것은 컴포넌트가 정확히 동일한 Props로 자주 리렌더링 되고, 리렌더링 로직이 비용이 많이 드는 경우에 유용하다.

주의사항

  • 컴포넌트의 props가 이전과 동일하더라도 내부에서 사용중인 Context가 변경되면 컴포넌트는 리렌더링된다.
  • 모든 props를 useMemo로 감싸지 않아도된다. primitive(원시) 값은 React.memo의 shallow 비교에서 "값 자체"로 비교되기 때문에 값이 같으면 메모화가 유지된다.

명시적으로 arePropsEqual를 선언하기

const Chart = memo(function Chart({ dataPoints }) {
  // ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
  return (
    oldProps.dataPoints.length === newProps.dataPoints.length &&
    oldProps.dataPoints.every((oldPoint, index) => {
      const newPoint = newProps.dataPoints[index];
      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
    })
  );
}
const Chart = memo(function Chart({ dataPoints }) {
  // ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
  return (
    oldProps.dataPoints.length === newProps.dataPoints.length &&
    oldProps.dataPoints.every((oldPoint, index) => {
      const newPoint = newProps.dataPoints[index];
      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
    })
  );
}
  • 드물지만 컴포넌트의 Props 변경을 최소화하는 것이 불가능할 수 있다. 이 경우 arePropsEqual를 제공하여 맞춤화된 비교를 제공할 수 있다.
  • arePropsEqual의 로직을 실행하는것이 렌더링을 하는것보다 느리다면, 다시 렌더링을 하는게 더 좋을수 있다.
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => console.log(count); // ⚠️ 이 시점의 `count`를 기억!

  return <Child onClick={handleClick} />;
}
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => console.log(count); // ⚠️ 이 시점의 `count`를 기억!

  return <Child onClick={handleClick} />;
}
  • arePropsEqual를 구현하는 경우 함수를 포함하여 모든 Prop를 비교해야 한다.
    • 위 예시에서 handleClick의 참조는 변하지 않아서 oldProps.onClick === newProps.onClick로만 비교하게 되면 영원히 true로 반환하게된다.
    • 하지만 count값이 변하더라도 handleClick의 참조는 변하지 않아서true를 리턴하게 되므로 계속해서 이전 count를 리턴하는 버그로 이어지게 된다.

startTransition

startTransition(action);
startTransition(action);
  • startTransition는 UI의 일부를 백그라운드에서 렌더링할 수 있다.
  • action: 하나 이상의 state를 업데이트하는 함수를 포함한다.
    • startTransition에 전달하는 일반함수는 즉시 호출되고 모든 State 업데이트를 Transition으로 표시한다.
    • Transitions으로 표시된 상태 업데이트는 non-blocking 방식으로 처리되며, 불필요한 로딩 표시가 나타나지 않는다.
  • startTransition만 사용하게 되면 Transition이 대기 중인지 알수 없다. 대기 중인 Transition을 표시하려면 useTransition를 통해 isPending를 사용한다.

"Transition으로 표시한다"의 뜻

  • _상태 업데이트를 "급하지 않은(non-urgent) 작업"으로 분류하여, 더 중요한 업데이트가 먼저 처리될 수 있도록 우선순위를 조정하는 것을 의미한다.
  • 기존 문제 : 복잡한 상태 업데이트가 메인 스레드를 블로킹하여 UI가 멈춘 듯한 느낌을 준다.
  • 개선 : startTransition으로 감싼 업데이트는 "전환(Transition)"으로 분류되어 즉시 처리되지 않고 중간에 더 중요한 업데이트가 끼어들 수 있다.
  • startTransition 내부의 상태 업데이트는 React의 "Transition" 큐에 등록된다.

Transition 큐

  • Concurrent Mode에서 사용되는 큐로 리액트 18버전 이후 도입되었다.

  • Transition Queue (트랜지션 큐),  Default Queue (기본 큐), Urgent Queue (긴급 큐) 로 나뉘며 우선순위에 따라 업데이트를 진행한다.

    • Urgent Queue → flushSync
    • Default Queue → 그 외
    • Transition Queue → startTransition

startTransition은 "동기 실행"만 추적한다.

startTransition(() => {
  setTimeout(() => {
    setState('new value'); // ⚠️ 이 업데이트가 낮은 우선순위로 처리되길 기대
  }, 1000);
});
startTransition(() => {
  setTimeout(() => {
    setState('new value'); // ⚠️ 이 업데이트가 낮은 우선순위로 처리되길 기대
  }, 1000);
});
  • 1초 뒤에 실행되는 setState('new value');는 인식할 수 없는데 그 시점엔 이미 startTransition의 실행 컨텍스트가 끝났기 때문이다.
startTransition(async () => {
  await fetchSomeData();
  setState('data loaded'); // ⚠️
});
startTransition(async () => {
  await fetchSomeData();
  setState('data loaded'); // ⚠️
});
  • await으로 비동기 흐름이 생기는 순간, setState('data loaded')는 이미 startTransition의 영향력에서 벗어나서 Transition으로 간주하지 않는다.
  • setState가 논리적으로 startTransition 안에 있어도, 실제로 실행되는 순간이 startTransition 범위 밖이면, React는 트랜지션으로 간주하지 않는다.

use

const value = use(resource);
const value = use(resource);
  • use는 Promise나 Context와 같은 데이터를 참조한다.
  • use는 Hook이 아니라서 다른 React Hook과 달리 useif와 같은 조건문과 반복문 내부에서 호출할 수 있다.
  • React 컴포넌트나 Hook 함수 내부에서 호출할 수 있으며 Hook 함수 외부나 try-catch 블록에서 use를 호출할 수는 없다.
    • React는 use를 통해 비동기 데이터를 기다릴 때 "렌더링 경로"를 예측 가능하게 하려한다.
    • 근데 try-catch 블록 안에 있으면 그 타이밍을 예측할 수 없어서 렌더링 타이밍이 깨질 수 있다.
  • use에 전달된 Promise가 대기하는 동안 use를 호출하는 컴포넌트는 Suspend 된다. use를 호출하는 컴포넌트가 Suspense 경계로 둘러싸여 있으면 Fallback이 표시된다.

사용예시

import { use } from 'react';

function Page() {
  const dataPromise = fetchData(); // 바로 실행되지만 아직 안 기다림
  const data = use(dataPromise); // 여기서 데이터 기다림
  return <div>{data.title}</div>;
}
import { use } from 'react';

function Page() {
  const dataPromise = fetchData(); // 바로 실행되지만 아직 안 기다림
  const data = use(dataPromise); // 여기서 데이터 기다림
  return <div>{data.title}</div>;
}
  • 데이터 요청결과를 렌더링에 활용하고자 할때 dataPromise를 직접쓰게 되면 Promise<T>의 타입을 가지고 있어 바로 참조가 불가능하다.
  • 이때 use를 쓰게 되면 값을 전부 받아올때까지 기다리므로 data.title과 같이 참조가 가능하다.

서버에서 클라이언트로 데이터 스트리밍하기

import { fetchMessage } from './lib.js';
import { Message } from './message.js';

export default function App() {
  const messagePromise = fetchMessage();
  return (
    <Suspense fallback={<p>waiting for message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}
import { fetchMessage } from './lib.js';
import { Message } from './message.js';

export default function App() {
  const messagePromise = fetchMessage();
  return (
    <Suspense fallback={<p>waiting for message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}
// message.js
import { use } from 'react';

export function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}
// message.js
import { use } from 'react';

export function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}
  • 클라이언트 컴포넌트에서 Promise를 생성하는 것보다 서버 컴포넌트에서 Promise를 생성하여 클라이언트 컴포넌트에 전달하는 것이 더 좋다.
    • 클라이언트 컴포넌트에서 생성된 Promise는 렌더링할 때마다 다시 생성되어 매번 요청된다.
    • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 Promise는 리렌더링 시에도 동일한 참조를 유지하고 재요청하지 않는다.
  • React는 use에 전달된 Promise를 메모이제이션한다. 동일한 Promise가 다시 전달되면 이전에 리졸브된 값을 재사용한다.
읽어주셔서 감사합니다