리액트로 웹 애플리케이션을 개발하면서 데이터를 가져오는 로직을 작성하다 보면, 가장 먼저 떠오르는 방법이 ‘useEffect’다. 컴포넌트가 마운트되거나 상태가 변경될 때 비동기 API를 호출하고, 받아온 데이터를 가지고 상태를 업데이트해서 화면에 반영하는 방식이 비교적 간단하기 때문이다. 그러나 프로젝트가 커지고 API 호출 횟수가 많아질수록 반복적인 useEffect 사용은 여러 문제점을 초래한다. 예를 들어, 새로고침할 때마다 동일한 API가 중복 호출되거나, 여러 컴포넌트가 같은 데이터를 요청해 네트워크 자원을 낭비하는 상황이 발생할 수 있다. 이는 결국 서버 부하 증가와 사용자 경험의 저하로 이어진다. <출처: SWR 공식 홈페이지> 이 문제를 해결하기 위해서는 보다 효율적으로 데이터 페칭을 관리할 수 있는 SWR 라이브러리를 고려할 수 있다. SWR은 “Stale-While-Revalidate” 전략을 통해 자동 캐싱과 백그라운드 갱신, 자동 갱신 등을 제공하여, useEffect만으로 구현하던 비효율적인 데이터 페칭 로직을 훨씬 간결하게 만들 수 있다. 이번 글에서는 useEffect 방식의 한계를 살펴보고, SWR이 어떤 방식으로 이를 보완할 수 있는지 알아보고자 한다. useEffect를 사용한 데이터 페칭 코드의 문제점import { useEffect, useState } from "react"; function ComponentA() { const [data, setData] = useState(null); useEffect(() => { fetch("/api/data") // API를 호출 .then((res) => res.json()) .then((result) => setData(result)); }, []); return <div>Component A Data: {JSON.stringify(data)}</div>; } function ComponentB() { const [data, setData] = useState(null); useEffect(() => { fetch("/api/data") // 동일한 API가 중복 호출됨 .then((res) => res.json()) .then((result) => setData(result)); }, []); return <div>Component B Data: {JSON.stringify(data)}</div>; } export { ComponentA, ComponentB }; useEffect 훅은 매우 유용하다. 다만 여러 컴포넌트에서 같은 데이터를 요청하게 되면 차츰 최적화를 고민하게 된다. 성능도 아쉽지만, 서로 다른 컴포넌트에서 데이터를 호출하는 타이밍의 차이에 의해 각 컴포넌트에서 서로 다른 데이터로 상태 업데이트를 하게 될 수도 있다. 좀 더 자세히 들여다보자. 코드가 길고 복잡해진다useEffect를 사용한 데이터 페칭은 일견 단순해 보이지만, 로딩 상태(isLoading), 에러 처리(isError), 데이터 반영(data)을 모두 한꺼번에 다루려고 할 때 코드가 빠르게 복잡해진다. 게다가 만약 API가 여러 개라면, useEffect와 useState가 계속 늘어나 가독성이 점차 떨어진다. import React, { useState, useEffect } from 'react'; interface User { id: number; name: string; email: string; } function UserProfile() { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState<boolean>(false); const [isError, setIsError] = useState<boolean>(false); useEffect(() => { let isMounted = true; setIsLoading(true); fetch('https://api.example.com/user/1') .then((response) => response.json()) .then((data: User) => { if (isMounted) { setUser(data); setIsLoading(false); } }) .catch(() => { if (isMounted) { setIsError(true); setIsLoading(false); } }); return () => { isMounted = false; }; }, []); if (isLoading) return <div>Loading...</div>; if (isError) return <div>Error!</div>; if (!user) return <div>No data</div>; return ( <div> <p>Name: {user.name}</p> <p>Email: {user.email}</p> </div> ); } export default UserProfile; 이 코드 자체는 기본 구조이지만, 프로젝트가 커질수록 각종 데이터 병합, 조건부 요청 등의 상황이 추가되면서 점점 더 복잡해진다. 중복 호출을 방지하기 어렵다서버에서 동일한 데이터를 여러 컴포넌트가 공유해야 할 때, 각 컴포넌트가 독립적으로 API를 호출하는 구조라면 중복 요청이 쉽게 발생한다. 별도의 캐싱 로직을 수동으로 작성하지 않는 한, 동일한 URL에 대한 API 요청이 여러 번 실행되므로 서버에 불필요한 부하를 준다. 또한 새로고침 시에도 매번 다시 데이터를 받아오므로 네트워크 리소스 사용이 비효율적이다. 자동 갱신 기능이 없다useEffect는 의존성 배열이 변경되거나 컴포넌트가 마운트될 때만 실행된다. 만약 네트워크가 연결되거나 브라우저 탭이 다시 포커스될 때마다 자동으로 데이터를 갱신하고 싶다면, 추가적인 로직을 직접 작성해야 한다. 이 또한 프로젝트 규모가 커지면 로직이 분산되고 중복돼 유지보수가 어려워진다. SWR을 선택하게 된 이유중복 호출을 방지하고 데이터의 일관성을 유지하는 방법에는 여러 가지가 있다. 가장 기본적인 방법은 공통된 상위 컴포넌트에서만 API를 호출하고, 그 결과를 하위 컴포넌트에 props로 전달하는 방식이다. 또는 Redux 같은 상태 관리 라이브러리를 활용하여, 전역 스토어를 데이터 저장소로 사용하는 방법도 있다. 개인적으로 Redux를 사용했던 경험이 크게 나쁘지는 않았지만, Action, Reducer, Dispatch, Subscribe 등 많은 코드가 필요하고, 구조가 복잡해진다는 점에 공감했다. 또한 API 요청 상태를 Redux에서 관리하려면 redux-thunk 또는 redux-saga 같은 미들웨어를 추가해야 하고, 자동 캐싱이나 중복 요청 방지 같은 기능이 기본적으로 제공되지 않는다. 이런 점에서 Redux는 API 요청 최적화보다는 전역 상태 관리를 위한 도구라는 점을 깨달았다. 이러한 고민 끝에 SWR을 사용하게 되었다. SWR은 API 요청을 자동으로 관리하며, 캐싱, 데이터 동기화, 중복 요청 방지 등의 기능을 기본적으로 제공한다. 특히 필요한 컴포넌트에서 직접 API를 호출하면서도 중복 요청을 방지할 수 있다는 점에서 내가 원했던 방식과 잘 맞았다. Redux처럼 별도의 상태 관리 로직을 작성하지 않아도 되고, 데이터가 최신 상태로 유지되며 자동으로 갱신되는 점이 SWR을 더욱 매력적인 도구로 만들었다. 게다가 기존 상태 관리 라이브러리를 완전히 대체하지 않고, 보완적으로 함께 사용할 수도 있다는 점도 마음에 들었다. API 요청 및 데이터 캐싱은 SWR이 담당하고, Redux 같은 전역 상태 관리 라이브러리는 UI 상태나 클라이언트 상태 관리에 활용하는 방식이 가능하다. 이를 통해 각 라이브러리의 강점을 살리면서 더 효율적인 상태 관리가 가능하다는 점이 좋았다. SWR의 동작 원리: Stale-While-Revalidate 전략 SWR은 “Stale-While-Revalidate” 기법을 사용하여 데이터의 최신성과 성능을 함께 확보한다. 단순히 데이터를 받아오는 데 그치지 않고, 캐시와 백그라운드 갱신을 통해 사용자가 항상 빠른 응답성과 최신 정보를 동시에 얻을 수 있도록 한다. 자동 캐싱useSWR 훅을 통해 특정 키(key)로 데이터를 요청하면, SWR 내부에서 해당 키와 함께 받은 데이터를 캐싱한다. 따라서 여러 컴포넌트가 동일한 키로 useSWR를 호출해도 실제 API는 한 번만 호출되고, 이후에는 캐시된 결과를 즉시 반환한다. 백그라운드 데이터 동기화캐시에 저장된 데이터가 우선 화면에 표시되지만, 동시에 백그라운드에서 실제 API에 접근해 최신 데이터를 가져오고, 가져온 최신 데이터를 통해 화면을 자동으로 업데이트한다. 따라서 사용자는 긴 로딩 시간 없이 즉각적인 피드백을 받고, 곧바로 갱신된 데이터로 변경되는 것을 볼 수 있다. 데이터 재검증 및 갱신브라우저 탭이 비활성화됐다가 다시 활성화되거나, 네트워크가 끊겼다가 다시 연결될 때 SWR은 자동으로 데이터를 재검증한다. 이는 SWR의 고유 설정인 revalidateOnFocus, revalidateOnReconnect 등을 통해 간단히 제어가 가능하다. import React from 'react'; import useSWR from 'swr'; interface User { id: number; name: string; email: string; } const fetcher = (url: string) => fetch(url).then((res) => res.json()); function UserProfile() { const { data, error } = useSWR<User>('/api/user', fetcher); if (error) return <div>Error!</div>; if (!data) return <div>Loading...</div>; return ( <div> <p>Name: {data.name}</p> <p>Email: {data.email}</p> </div> ); } export default UserProfile; 위 예시처럼 useSWR('/api/user', fetcher)를 호출하면, SWR은 /api/user로 한 번 API를 호출한 뒤 해당 데이터를 자동으로 캐싱한다. 다른 컴포넌트에서 동일 키를 사용하면 중복 요청을 하지 않고, 캐시된 데이터를 즉시 보여준다. 탭 포커스를 옮기거나 네트워크가 재연결되면, 자동으로 최신 데이터를 다시 가져오는 것도 큰 장점이다. 데이터가 자주 변경될 필요가 없는 웹페이지에 SWR을 적용한 사례기존 문제: 불필요한 API 요청으로 인한 성능 저하일반적으로 웹 페이지의 데이터는 사용자들의 행동에 의해 업데이트된다. 데이터가 업데이트되는 평균 주기는 사용자들의 활동 패턴에 따라 달라진다. 만약 데이터가 자주 변하지 않음에도 매번 useEffect로 동일한 API를 호출하면, 새로고침 시마다 불필요한 요청이 발생하고 서버 부하가 커질 수 있다. 게다가 여러 컴포넌트가 중복으로 같은 정보를 요청한다면 네트워크 리소스 사용량이 크게 증가한다. 최근 사이드 프로젝트로 작업하고 있는 툴에서는 사용자들이 캔버스에 함께 접속해서 같이 그림을 그리고, 그 캔버스를 저장할 수 있는 기능을 만들고 있다. 캔버스 목록을 한눈에 보여주는 테이블 형태의 대시보드가 있고, 각 행을 클릭하면 해당 대시보드의 최신 버전 url로 이동하는 구조다. 테이블 하단에는 페이지네이션이 있는데, 페이지를 이동할 때마다 API가 매번 새로 요청되는 것이 과하다는 생각이 들었다. 만약 여기에 필터와 정렬 기능까지 추가로 구현된다면 부하는 더욱 심해질 것 같았다. 찾아보니 SWR에는 페이지네이션을 고려한 인터페이스가 존재했다. 문서에 따르면 SWR의 캐시로 인해 다음 페이지를 프리로드해주는 이점도 있다고 한다. SWR 적용: 자동 캐싱과 최적화된 데이터 갱신function App () { const [pageIndex, setPageIndex] = useState(0); // React state인 페이지 인덱스를 포함하는 API URL const { data } = useSWR(`/api/canvasList?page=${pageIndex}`, fetcher); // ... 로딩 및 에러 상태를 처리 return <div> {data.map(item => <div key={item.id}>{item.name}</div>)} <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button> <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button> </div> }<출처: SWR 공식 홈페이지> 자동 캐싱: /api/canvasList?page=${pageIndex} 키로 불러온 데이터는 캐시에 저장된다. 이후 같은 키(또는 동일한 파라미터)로 useSWR를 호출하면 추가 API 요청 없이 즉시 캐시된 데이터를 반환하여 렌더링하므로 사용자가 체감하는 로딩 시간을 최소화할 수 있다.최신 데이터 동기화: 사용자가 브라우저 탭을 옮겼다가 돌아오거나(revalidateOnFocus), 네트워크가 끊겼다가 다시 연결될 때(revalidateOnReconnect) 자동으로 데이터를 재검증한다. 이 외에도 데이터 자동 갱신을 위한 여러 옵션들이 있어, 원한다면 인터벌 값 등을 세세하게 지정할 수 있다. 자세한 API 옵션은 문서에서 확인할 수 있다.불필요한 트래픽 절약: 실제로 자주 변경되지 않는 데이터에 대해 매번 새로고침할 때마다 API를 다시 호출하지 않도록 해, 네트워크 부하와 서버 트래픽 모두 줄어든다.사실 사이드 프로젝트라서 API를 호출하는 양 자체가 많지도 않고, 실제 사용자들의 경험을 듣기 어려운 부분이 있다. 하지만 실제로 API 호출의 감소율을 봤을 때 눈에 띄는 변화가 보였고, 만약 유저가 많아진다면 그로 인한 비용은 크게 차이가 날 것이다. 사이드 프로젝트를 하더라도 실제 서비스로의 확장 가능성을 염두에 두기 때문에, 유의미한 개선이라고 판단했다. SWR로 효율적인 데이터 페칭을 구현하자 useEffect를 이용한 전통적인 데이터 페칭 방식은 간단하게 보이지만, 규모가 커질수록 중복 호출과 상태 관리 복잡도 증가로 인해 성능과 가독성 면에서 문제를 일으킨다. 이를 전역 상태 관리를 통해 해결할 수도 있지만, 더 간단한 방법을 찾는다면 SWR을 고려해 볼만 하다. SWR은 “Stale-While-Revalidate” 전략으로 자동 캐싱과 백그라운드 동기화를 제공해, 이러한 문제점을 효과적으로 개선한다. 단, SWR이 useEffect를 완전히 대체한다고 볼 수는 없다. 사용자 입력이나 특정한 의존성에 의해 즉각적으로 반응해야 하는 로직은 여전히 useEffect나 별도 훅이 필요하다. 그럼에도 반복적인 API 호출과 공통 데이터 중복 관리 문제를 겪고 있다면, SWR을 적극적으로 도입해 봐도 좋겠다. 만약 프로젝트에서 useEffect가 난무하고, API 호출이 불필요하게 자주 발생하는 상황이라면 SWR을 적용함으로써 ‘개발 효율’과 ‘성능 개선’이라는 두 마리 토끼를 모두 잡을 수 있을 것이다. 실제 현업 환경에서 자주 변경되지 않는 페이지가 있는지 둘러보고, 차근차근 SWR을 적용해 보는 것을 추천한다. ©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.