리액트(React)에서 시간이 오래 걸리는 작업은 대부분 비동기 처리(asynchronous processing)와 관련된다. 리액트는 UI를 빠르게 반응시키는 것이 핵심이기 때문에, 무거운 작업이 메인 스레드를(blocking) 막지 않도록 비동기 처리나 별도 스레드로 분리해야 한다.
1. 비동기로 처리하는 이유
리액트는 기본적으로 싱글 스레드(Single Thread) 환경에서 동작한다. 즉, UI 렌더링과 자바스크립트 실행이 같은 스레드에서 처리된다. 따라서, 아래 작업이 동기적으로 실행되면 화면이 멈추거나 클릭이 먹지 않는 UI 프리징(freezing) 현상이 발생한다.
- 무거운 연산: 대용량 데이터 계산, 복잡한 정렬 등
- 네트워크 요청: API 호출
- 파일 입출력: 이미지 업로드 등
그래서 이런 작업은 비동기 처리 또는 별도 스레드(Web Worker)로 분리해야 한다.
2. 대표적인 비동기 처리 방식
| 목적 | 방법 |
| API 요청 | fetch, axios, async/await |
| 지연 실행 / 애니메이션 | setTimeout, setInterval, requestAnimationFrame |
| 복잡한 계산 분리 | Web Worker |
| 비동기 이벤트 | Promise, async 함수 |
3. 비동기 처리 예시코드로 이해하기
3-1. useEffect + async/await (API 요청)
비동기 API 호출이나 타이머 작업은 보통 useEffect 안에서 처리한다. cleanup과 에러 처리까지 포함하면 아래와 같이 구성할 수 있다.
import { useEffect, useState } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchUsers = async () => {
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('API 요청 실패');
const data = await res.json();
if (isMounted) setUsers(data);
} catch (err) {
if (isMounted) setError(err.message);
}
};
fetchUsers();
return () => { isMounted = false; };
}, []);
if (error) return <div>{error}</div>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
3-2. setTimeout, setInterval, requestAnimationFrame (지연 실행 / 애니메이션)
UI 애니메이션이나 반복 처리 작업은 비동기 타이머를 사용한다.
import { useEffect, useState } from 'react';
function Countdown() {
const [count, setCount] = useState(10);
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev - 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <div>카운트다운: {count}</div>;
}
requestAnimationFrame은 애니메이션 프레임 단위로 UI를 갱신할 때 사용한다.
function animateBox() {
let pos = 0;
function step() {
pos += 2;
document.getElementById('box').style.left = pos + 'px';
if (pos < 300) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
3-3. Web Worker (무거운 계산 분리)
CPU 연산이 많은 작업은 Web Worker로 분리한다. 리액트에서는 worker와 상태 업데이트를 연결해 사용할 수 있다.
// worker.js
self.onmessage = (e) => {
const result = heavyCompute(e.data);
self.postMessage(result);
};
// React Component
import { useEffect, useState } from 'react';
function HeavyTask({ input }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(input);
worker.onmessage = (e) => setResult(e.data);
return () => worker.terminate();
}, [input]);
return <div>결과: {result}</div>;
}
3-4. Promise / async 이벤트 처리
사용자 인터랙션에 따른 비동기 작업도 처리한다.
function SaveButton() {
const handleClick = async () => {
try {
await saveDataToServer();
alert('저장 완료!');
} catch (err) {
alert('저장 실패: ' + err.message);
}
};
return <button onClick={handleClick}>저장</button>;
}
3-5. Suspense + React.lazy (비동기 컴포넌트)
React 18 이상에서는 비동기 컴포넌트를 UI 흐름에 통합할 수 있다.
import React, { Suspense } from 'react';
const Profile = React.lazy(() => import('./Profile'));
function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<Profile />
</Suspense>
);
}
Suspense는 로딩 중 상태를 표현하고, 컴포넌트 렌더링이 완료되면 자동으로 교체한다.
3-6. useTransition (UI 렌더링 우선순위 분리)
렌더링이 오래 걸리는 작업은 useTransition을 사용해 우선순위를 낮출 수 있다.
import { useTransition, useState } from 'react';
function SearchList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
startTransition(() => {
setQuery(value);
});
};
const filtered = items.filter(item => item.includes(query));
return (
<>
<input onChange={handleChange} />
{isPending ? <div>로딩 중...</div> : <ul>{filtered.map(i => <li key={i}>{i}</li>)}</ul>}
</>
);
}
사용자가 입력한 값은 즉시 반영하고, 리스트 필터링 렌더링은 백그라운드에서 비동기 처리된다.
4. React 비동기 처리 요약
리액트는 싱글 스레드 환경에서 동작하기 때문에, 오래 걸리는 연산이나 네트워크 요청은 비동기 처리 또는 별도 스레드 분리가 필수적이다.
상황별로 적용할 수 있는 대표적인 비동기 처리 및 최적화 방법을 요약하면 아래와 같다.
| 상황 | 해결 방법 | 사용 예시 / 주요 기술 |
| API 요청 | async/await, fetch, axios | useEffect 안에서 비동기 데이터 호출 및 상태 업데이트 |
| UI 애니메이션 / 지연 처리 | setTimeout, setInterval, requestAnimationFrame | 애니메이션, 카운트다운, 일정 주기 업데이트 등 |
| CPU가 무거운 계산 | Web Worker | 이미지 압축, 복잡한 수학 연산, 대규모 데이터 가공 |
| 사용자 이벤트 비동기 처리 | Promise, async 함수 | 버튼 클릭 후 API 호출, 저장/검증 로직 처리 |
| 비동기 컴포넌트 로딩 | React.lazy, Suspense | 동적 import, 코드 분할(Code Splitting) |
| 렌더링이 느린 경우 | useTransition, memo, useMemo | 대규모 리스트 렌더링, 입력 지연 최소화 |
| 반복 렌더링 최적화 | React.memo, useCallback, useMemo | 동일한 props로 불필요한 재렌더링 방지 |
- 비동기 처리의 핵심 목적은 “UI 멈춤 방지”와 “렌더링 성능 향상”이다.
- I/O 지연(API, 파일, 애니메이션)은 async/await 기반의 비동기 처리로, CPU 연산은 Web Worker로 분리한다.
- 렌더링 최적화는 React의 Concurrent Feature(useTransition, Suspense)와 메모이제이션(useMemo, React.memo)을 함께 사용하는 것이 효과적이다.
'Frontend > React' 카테고리의 다른 글
| useEffect vs useLayoutEffect (0) | 2025.10.27 |
|---|---|
| 상태(stateful) & 비상태(stateless) 컴포넌트 (0) | 2025.10.25 |
| useRef 자세히 알아보기 (0) | 2025.10.24 |
| useEffect의 cleanup 함수 알아보기 (0) | 2025.10.24 |
| React 메모이제이션과 최적화 (0) | 2025.10.24 |