useEffect를 이용한 데이터 가져오기
비동기 처리 기술을 활용하여 리액트 컴포넌트 내부에서 데이터를 가져오는(Data Fetching) 방법에 대해 알아보겠습니다.
리액트에서 컴포넌트가 마운트되거나 업데이트될 때 외부에서 데이터를 가져와야 하는 경우가 많습니다. 이러한 "부수 효과(Side Effects)"를 처리하기 위해 리액트는 useEffect
훅을 제공합니다. 이 장에서는 useEffect
훅을 사용하여 API 호출 등의 비동기 작업을 수행하는 방법을 상세히 다루겠습니다.
useEffect
훅 복습
useEffect
훅은 함수 컴포넌트 내에서 부수 효과(Side Effects) 를 수행할 수 있게 해줍니다. 데이터 페칭, 구독 설정, DOM 직접 조작 등이 부수 효과에 해당합니다.
useEffect
는 다음과 같은 형태로 사용됩니다.
useEffect(() => {
// 부수 효과 코드
// 이 함수는 컴포넌트가 렌더링된 후에 실행됩니다.
return () => {
// 클린업(Clean-up) 함수 (선택 사항)
// 컴포넌트가 언마운트되거나 다음 효과가 실행되기 전에 실행됩니다.
// 구독 해제, 타이머 클리어 등 정리 작업을 수행합니다.
};
}, [dependencies]); // 의존성 배열 (선택 사항)
- 의존성 배열(
[dependencies]
)- 배열이 없으면 (또는 빈 배열이 아님) → 모든 렌더링마다 효과가 실행됩니다.
- 빈 배열(
[]
) → 컴포넌트가 처음 마운트될 때 한 번만 실행되고, 언마운트될 때 클린업 함수가 실행됩니다. (초기 데이터 로딩에 적합) - 값들이 있는 배열(
[dep1, dep2]
) → 배열 안의 값들 중 하나라도 변경될 때마다 효과가 다시 실행됩니다.
데이터 페칭의 기본 원리
데이터 페칭은 대표적인 비동기 부수 효과입니다. useEffect
를 사용하여 데이터를 가져올 때의 일반적인 패턴은 다음과 같습니다.
- 컴포넌트 마운트 시 데이터 로딩: 빈 의존성 배열
[]
을 사용하여 컴포넌트가 처음 로드될 때 한 번만 데이터를 가져옵니다. - 데이터 로딩 상태 관리: 데이터를 가져오는 중인지, 성공했는지, 실패했는지 사용자에게 피드백을 주기 위해
useState
를 사용하여 로딩 상태, 에러 상태, 데이터 상태를 관리합니다. - 클린업 함수 활용 (옵션):
useEffect
의 클린업 함수를 사용하여 비동기 요청이 더 이상 필요 없을 때(예: 컴포넌트가 언마운트될 때) 해당 요청을 취소하거나 정리합니다. 이는 메모리 누수나 "컴포넌트가 언마운트된 후 상태 업데이트" 경고를 방지합니다.
실제 예제: 사용자 목록 가져오기
JSONPlaceholder API를 사용하여 사용자 목록을 가져오는 컴포넌트를 만들어 봅시다.
기본 Fetch API 사용 예제
// src/components/UserList.js
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]); // 사용자 데이터를 저장할 상태
const [loading, setLoading] = useState(true); // 로딩 상태
const [error, setError] = useState(null); // 에러 상태
useEffect(() => {
// 데이터를 가져오는 비동기 함수 정의 (async/await 사용)
const fetchUsers = async () => {
try {
setLoading(true); // 요청 시작 시 로딩 상태를 true로 설정
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) { // HTTP 상태 코드가 200번대가 아니면 에러 발생
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json(); // 응답을 JSON 형태로 파싱
setUsers(data); // 데이터 상태 업데이트
} catch (err) {
setError(err); // 에러 발생 시 에러 상태 업데이트
} finally {
setLoading(false); // 요청 완료(성공/실패 무관) 시 로딩 상태를 false로 설정
}
};
fetchUsers(); // 비동기 함수 호출
// 클린업 함수는 필요에 따라 추가
// 여기서는 fetch 요청을 취소하는 기능은 포함하지 않습니다. (AbortController 사용 시 필요)
return () => {
console.log('UserList 컴포넌트 클린업');
};
}, []); // 빈 의존성 배열: 컴포넌트가 마운트될 때 한 번만 실행
if (loading) {
return <div style={{ textAlign: 'center', padding: '20px' }}>사용자 정보를 불러오는 중...</div>;
}
if (error) {
return <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>오류 발생: {error.message}</div>;
}
return (
<div style={{ maxWidth: '800px', margin: '20px auto', padding: '20px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<h2 style={{ textAlign: 'center', color: '#2c3e50' }}>사용자 목록</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
{users.map(user => (
<li
key={user.id}
style={{
padding: '15px',
marginBottom: '10px',
border: '1px solid #eee',
borderRadius: '5px',
backgroundColor: '#fefefe',
boxShadow: '0 1px 3px rgba(0,0,0,0.02)',
}}
>
<strong style={{ color: '#3498db' }}>{user.name}</strong> ({user.username})
<br />
<small>{user.email}</small>
</li>
))}
</ul>
</div>
);
}
export default UserList;
App.js
에 추가
// src/App.js (일부)
import React from 'react';
import UserList from './components/UserList'; // UserList 임포트
function App() {
return (
<div className="App">
{/* ... 기존 Navbar, Routes 등 */}
<UserList /> {/* UserList 컴포넌트 추가 */}
</div>
);
}
실행 및 확인:
UserList
컴포넌트를 App.js
에 추가하고 실행하면, "사용자 정보를 불러오는 중..." 메시지가 잠시 표시된 후, JSONPlaceholder에서 가져온 사용자 목록이 나타나는 것을 볼 수 있습니다. 네트워크 탭에서 실제 API 요청이 발생하는 것을 확인할 수도 있습니다.
async/await
사용 시 주의사항
useEffect
훅에 전달되는 함수는 동기 함수여야 합니다. 따라서 useEffect
콜백 함수를 직접 async
로 만들면 안 됩니다.
잘못된 예시
useEffect(async () => { // 🚨 이렇게 직접 async로 만들지 마세요!
const response = await fetch(...);
const data = await response.json();
// ...
}, []);
async
함수는 항상 Promise를 반환하는데, useEffect
는 반환된 Promise를 클린업 함수로 간주하지 않습니다. 이는 예상치 못한 동작을 유발할 수 있습니다.
올바른 방법:
useEffect
내부에서 비동기 함수를 정의하고, 즉시 호출하는 패턴을 사용합니다.
useEffect(() => {
const fetchData = async () => { // 🌟 useEffect 내부에서 async 함수를 정의
// ... 비동기 로직
};
fetchData(); // 🌟 정의된 async 함수를 즉시 호출
}, []);
위 UserList.js
예제에서 사용한 방식이 바로 이 올바른 패턴입니다.
의존성 배열에 따른 데이터 페칭
데이터 페칭은 항상 컴포넌트 마운트 시점에만 필요한 것은 아닙니다. 예를 들어, 페이지네이션이나 필터링처럼 특정 값이 변경될 때마다 데이터를 다시 가져와야 하는 경우도 있습니다.
예시: 사용자 ID에 따른 특정 사용자 정보 가져오기
// src/components/UserProfileById.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; // React Router 사용 예시
function UserProfileById() {
const { userId } = useParams(); // URL 파라미터에서 userId 가져옴
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId) { // userId가 없는 경우 (예: 초기 렌더링 시점에 파라미터가 아직 없을 때)
setLoading(false);
setError(new Error('사용자 ID가 제공되지 않았습니다.'));
return;
}
const fetchUser = async () => {
try {
setLoading(true);
// userId가 변경될 때마다 새로운 URL로 요청
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchUser();
return () => {
// 클린업: 이전 요청이 완료되기 전에 userId가 변경되면
// 이전 요청의 응답으로 인한 상태 업데이트를 방지할 수 있습니다.
// (실제 fetch API는 AbortController 필요)
console.log(`UserProfileById: Clean-up for userId ${userId}`);
};
}, [userId]); // 🌟 userId가 변경될 때마다 useEffect 재실행
if (loading) {
return <div style={{ textAlign: 'center', padding: '20px' }}>사용자 정보를 불러오는 중...</div>;
}
if (error) {
return <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>오류 발생: {error.message}</div>;
}
if (!user) {
return <div style={{ textAlign: 'center', padding: '20px' }}>사용자를 찾을 수 없습니다.</div>;
}
return (
<div style={{ maxWidth: '600px', margin: '20px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '20px' }}>{user.name}님의 프로필</h2>
<p><strong>아이디:</strong> {user.username}</p>
<p><strong>이메일:</strong> {user.email}</p>
<p><strong>전화번호:</strong> {user.phone}</p>
<p><strong>웹사이트:</strong> {user.website}</p>
<p><strong>회사:</strong> {user.company.name}</p>
</div>
);
}
export default UserProfileById;
라우트 설정 (App.js 또는 관련 라우트 파일)
// src/App.js (일부)
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import UserProfileById from './components/UserProfileById'; // 새 컴포넌트 임포트
// ... 다른 임포트
function App() {
return (
<BrowserRouter>
{/* ... Navbar 등 */}
<div className="main-content">
<Routes>
{/* ... 다른 라우트들 */}
{/* 동적 라우트 설정 */}
<Route path="/users/:userId" element={<UserProfileById />} />
{/* ... 404 라우트 */}
</Routes>
</div>
</BrowserRouter>
);
}
실행 및 확인:
http://localhost:3000/users/1
또는 http://localhost:3000/users/5
와 같이 userId
를 변경해가며 접속하면, 해당 ID의 사용자 정보가 다시 로드되는 것을 확인할 수 있습니다. 이는 userId
가 의존성 배열에 포함되어 있기 때문입니다.
클린업 함수와 요청 취소 (AbortController)
컴포넌트가 언마운트되기 전에 비동기 요청이 완료되지 않은 경우, 해당 요청의 응답이 도착하여 언마운트된 컴포넌트의 상태를 업데이트하려고 하면 React에서 경고를 발생시킵니다 (메모리 누수 가능성). 이를 방지하기 위해 AbortController
를 사용하여 비동기 요청을 취소할 수 있습니다.
// src/components/CancellableFetch.js
import React, { useState, useEffect } from 'react';
function CancellableFetch() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // AbortController 인스턴스 생성
const signal = controller.signal; // 신호 가져오기
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', { signal }); // fetch 옵션에 signal 전달
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
// AbortError는 요청 취소로 인한 에러이므로 특별히 처리할 수 있습니다.
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
// 클린업 함수: 컴포넌트 언마운트 또는 의존성 변경 시 실행
return () => {
console.log('클린업: 요청 취소 시도');
controller.abort(); // 요청 취소!
};
}, []); // 빈 배열: 마운트 시 한 번만 실행
if (loading) return <div style={{ textAlign: 'center', padding: '20px' }}>데이터 로딩 중...</div>;
if (error) return <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>에러: {error.message}</div>;
return (
<div style={{ maxWidth: '600px', margin: '20px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '20px' }}>게시글 상세</h2>
<h3 style={{ color: '#3498db', marginBottom: '10px' }}>{data.title}</h3>
<p>{data.body}</p>
<p style={{ marginTop: '15px', fontSize: '0.9em', color: '#777' }}>
(이 컴포넌트는 `AbortController`를 사용하여 마운트 해제 시 fetch 요청을 취소합니다.)
</p>
</div>
);
}
export default CancellableFetch;
이 CancellableFetch
컴포넌트를 테스트하려면, 이 컴포넌트를 렌더링하는 부모 컴포넌트를 만들어 일정 시간 후 언마운트되도록 하거나, 다른 페이지로 빠르게 이동하면서 네트워크 탭을 확인해 볼 수 있습니다. 요청이 취소되면 status
가 (canceled)
로 표시됩니다.
"useEffect
를 이용한 데이터 가져오기"는 여기까지입니다. 이 장에서는 useEffect
훅을 사용하여 리액트 컴포넌트 내부에서 비동기 데이터 페칭을 수행하는 방법을 상세하게 배웠습니다. 특히 async/await
와 함께 데이터 로딩, 에러 처리, 그리고 의존성 배열에 따른 재실행, 마지막으로 AbortController
를 이용한 요청 취소까지 중요한 패턴들을 익혔습니다.
useEffect
는 컴포넌트 생명주기와 밀접하게 관련되어 있어 강력하지만, 잘못 사용하면 무한 루프나 불필요한 재렌더링을 유발할 수 있으므로 주의 깊게 사용해야 합니다. 다음 장에서는 axios
와 같은 서드파티 라이브러리를 사용하여 더욱 편리하게 데이터를 가져오는 방법과, 데이터 페칭을 위한 커스텀 훅을 만드는 방법을 알아보겠습니다.