Vercel에서 만든 Data Fetching 라이브러리 SWR

안녕하세요, 소플입니다.

작년(2023년) 10월 react-querySWR의 npm trends에서 역전이 일어났습니다.

아래 차트를 보면 오랜기간 동안 우위를 차지하던 react-query를 SWR이 넘어선 것을 볼 수 있습니다.

react-query vs SWR npm trends

아직 GitHub Star 수는 10,000개 가량 차이가 나지만,

npm trends를 통해 점점 더 많은 개발자들이 SWR을 사용하고 있다는 사실을 알 수 있습니다.

도대체 SWR이 뭐길래 react-query까지 넘어선 걸까요?🤔

이번 매거진에서는 Data Fetching 라이브러리의 개념과 함께 SWR에 대해서 다뤄보도록 하겠습니다.


Data Fetching이란?

우리가 Data Fetching이라고 하면 일반적으로 서버로부터 데이터를 가져오는 것을 의미합니다.

프론트엔드 개발자의 관점에서는, 백엔드 개발자가 만든 API 서버로부터 데이터를 가져오는 것을 Data Fetching이라고 할 수 있습니다.

물론 여기에는 API 호출뿐만 아니라 다른 곳에서 가져오는 데이터들도 포함될 수 있습니다.

아래 코드는 데이터를 서버로부터 받아오는 간단한 리액트 컴포넌트 예시 코드입니다.

function MyComponent() {
    cosnt [data, setData] = useState(null);

    useEffect(() => {
        fetch('/api/data')
            .then((res) => res.json())
            .then((data) => setData(data));
    }, []);

    // 데이터를 받아오기 전까지는 로딩 상태 보여줌
    if (!data) {
        return <Spinner />;
    }
 
    // 데이터를 받아온 이후에는 실제 컴포넌트 렌더링
    return (
        <div>
            <Content data={data} />
        </div>
    );
}

100% 정적 데이터로 구성된 웹사이트가 아닌 이상, 대부분의 웹 애플리케이션은 서버로부터 데이터를 가져오게 됩니다.

그리고 이러한 과정은 애플리케이션 전체에 걸쳐서 수많은 곳에서 반복적으로 일어납니다.

즉, Data Fetching이라는 것이 웹 애플리케이션 개발에 큰 비중을 차지하는 것이죠.

그리고 이러한 Data Fetching 과정을 편리하게 만들어주는 것이 바로 Data Fetching 라이브러리입니다.

Data Fetching 라이브러리의 필요성

그렇다면 Data Fetching 라이브러리가 왜 필요할까요?

먼저 아래 한 가지 예시 코드를 살펴보도록 하겠습니다.

Page라는 상위 컴포넌트는 서버로부터 사용자의 정보를 받아와서 하위 컴포넌트인 Navbar, Contentprops로 전달하고 있습니다.

그리고 각각의 하위 컴포넌트는 이를 이용해서 화면에 나타날 요소들을 구성하고 있습니다.

function Page() {
    cosnt [user, setUser] = useState(null);

    useEffect(() => {
        fetch('/api/user')
            .then((res) => res.json())
            .then((data) => setUser(data));
    }, []);

    // 데이터를 받아오기 전까지는 로딩 상태 보여줌
    if (!user) {
        return <Spinner />;
    }
 
    // 데이터를 받아온 이후에는 실제 컴포넌트 렌더링
    return (
        <div>
            <Navbar user={user} />
            <Content user={user} />
        </div>
    );
}

function Navbar({ user }) {
    return (
        <div>
            <Avatar user={user} />
        </div>
    )
}
 
function Content({ user }) {
    return <h1>안녕하세요, {user.name}</h1>
}
 
function Avatar({ user }) {
    return <img src={user.avatar} alt={user.name} />
}

이처럼 여러 계층에 걸쳐서 props로 데이터를 전달하는 구조를 Prop Drilling이라고 부릅니다.

이러한 구조는 장점도 있지만 컴포넌트 사이에 데이터의 의존성이 생기고 깊이가 깊어질수록 단점이 더 커집니다.

그래서 보통 이러한 구조를 해결하기 위해서 리액트의 Context API나 Redux 같은 상태 관리 라이브러리를 사용하게 됩니다.

특정 컴포넌트가 컴포넌트 트리 상에 어디에 위치에 있는지 상관없이 데이터에 접근할 수 있게 해주는 것이죠.

하지만 지금 예시에서는 데이터가 서버에 존재하기 때문에 '필요할 때마다 서버에서 가져오면 되지 않나?' 라는 생각을 해볼 수 있습니다.

아래 코드처럼 말이죠.

function Page() {
    return (
        <div>
            <Navbar />
            <Content />
        </div>
    );
}

function Navbar() {
    return (
        <div>
            <Avatar />
        </div>
    )
}
 
function Content() {
    cosnt [user, setUser] = useState(null);

    useEffect(() => {
        fetch('/api/user')
            .then((res) => res.json())
            .then((data) => setUser(data));
    }, []);

    if (!user) {
        return <Spinner />;
    }

    return <h1>안녕하세요, {user.name}</h1>
}
 
function Avatar() {
    cosnt [user, setUser] = useState(null);

    useEffect(() => {
        fetch('/api/user')
            .then((res) => res.json())
            .then((data) => setUser(data));
    }, []);

    if (!user) {
        return <Spinner />;
    }

    return <img src={user.avatar} alt={user.name} />
}

이렇게 하게 되면 각 컴포넌트는 자신이 직접 서버로부터 데이터를 가져오기 때문에 상위 컴포넌트와의 데이터 의존성이 사라집니다.

그리고 의존성이 없기 때문에 애플리케이션 전체에 걸쳐서 어디에서나 이 컴포넌트들을 가져다 사용할 수 있겠죠.

여기서 반복되는 코드를 줄이기 위해서 동일한 코드를 아래와 같이 커스텀 훅(Custom hook) 으로 추출할 수도 있습니다.

// 추출한 커스텀 훅
function useUser() {
    cosnt [user, setUser] = useState(null);

    useEffect(() => {
        fetch('/api/user')
            .then((res) => res.json())
            .then((data) => setUser(data));
    }, []);

    return user;
}

function Page() {
    return (
        <div>
            <Navbar />
            <Content />
        </div>
    );
}

function Navbar() {
    return (
        <div>
            <Avatar />
        </div>
    )
}
 
function Content() {
    const user = useUser();  // 커스텀 훅 호출

    if (!user) {
        return <Spinner />;
    }

    return <h1>안녕하세요, {user.name}</h1>
}
 
function Avatar() {
    const user = useUser();  // 커스텀 훅 호출

    if (!user) {
        return <Spinner />;
    }

    return <img src={user.avatar} alt={user.name} />
}

이렇게 하니 반복되는 코드도 사라지고 깔끔해진 것을 볼 수 있습니다.

그런데 여기서 아래와 같은 문제가 발생하게 됩니다.

🚨 각각의 컴포넌트가 마운트 될 때마다 동일한 API에 대한 호출이 발생!

현재 useUser() 커스텀 훅이 호출될 때마다 동일한 API(/api/user)에 대한 호출이 반복적으로 발생합니다.

만약 이 훅을 다른 수많은 컴포넌트들에서 사용하게 되면, 그만큼 API 호출이 많이 발생하고 결국 서버의 부하가 늘어나게 됩니다.

이 때 우리는 아래와 같은 점을 고민해 볼 필요가 있습니다.

항상 최신 데이터를 유지하는 것이 필요하지만,
몇 초 사이에 서버의 데이터가 바뀔 일이 많이 있을까? 🤔

만약 서버의 데이터가 바뀔 일이 많지 않다면, 굳이 모든 컴포넌트에서 항상 최신 데이터를 가져올 필요가 없겠죠.

즉, 먼저 API를 호출한 누군가가 가져온 데이터를 캐싱 해놓고, 다른 컴포넌트들은 캐싱된 데이터를 가져다 쓰면 되는 것입니다.

그리고 이러한 전략을 편리하게 적용할 수 있게 해주는 것이 바로 Data Fetching 라이브러리입니다.

엄밀히 말하면 Data Fetching & Caching 라이브러리라고 하는 것이 맞겠죠.

그리고 이러한 라이브러리의 대표 주자가 바로 앞에 나왔던 react-querySWR입니다.

SWR 소개 및 기본 사용법

지금부터는 SWR에 대해서 살펴보도록 하겠습니다.

SWR은 Next.js를 만든 Vercel에서 만든 라이브러리입니다.

그래서인지 Next.js의 상승세와 더불어 더 많이 사용되는게 아닐까 라는 생각을 해봅니다ㅎㅎ

SWR이라는 이름은 HTTP RFC 5861에 의해 알려진 HTTP 캐시 무효 전략인 stale-while-revalidate에서 유래되었다고 합니다.

참고로 이 전략은 먼저 캐시(stale)로부터 데이터를 반환한 후, fetch 요청(revalidate)을 하고, 최종적으로 최신화된 데이터를 가져오는 전략입니다.

쉽게 말해서 캐싱된 데이터가 있으면 그걸 반환하고, 이후 최신 데이터를 받아와서 업데이트하는 것이죠.

SWR

그렇다면 SWR은 어떻게 사용해야 할까요?

SWR은 태생부터 리액트 훅 형태로 사용하도록 개발되었습니다.

그래서 아래와 같은 형태로 리액트 컴포넌트 내에서 곧바로 사용할 수 있습니다.

import useSWR from 'swr';

function Avatar() {
    const { isLoading, data, error } = useSWR('/api/user', fetcher);

    if (!isLoading) {
        return <Spinner />;
    }

    if (error) {
        return <div>에러 발생!</div>;
    }

    return <img src={user.avatar} alt={user.name} />;
}

여기서 fetcher는 서버로부터 데이터를 받아오는 역할을 하는 함수이며, 별도로 구현해야 합니다.

아래와 같이 fetch를 직접 사용해서 구현하거나, Axios 같은 라이브러리를 사용해서 구현하면 됩니다.

// fetch 사용
const fetcher = url => fetch(url).then((res) => res.json())
// Axios 사용
import axios from 'axios'
 
const fetcher = url => axios.get(url).then(res => res.data)

useSWR() 훅은 파라미터로 전달받은 API 주소와 fetcher를 사용해서 서버로부터 데이터를 받아오고 캐싱을 해둡니다.

그리고 이미 캐싱된 데이터와 동일한 API 호출이 발생하면, 서버에 다시 요청하지 않고 캐싱된 데이터를 반환합니다.

하지만 캐싱된 데이터가 주기적으로 최신화(revalidate) 될 필요가 있기 때문에, 설정된 옵션에 따라 서버로부터 데이터를 받아오게 됩니다.

예를 들면, 다른 탭에 갔다가 다시 돌아오거나, 특정 주기마다 갱신하거나, 다시 온라인 상태가 되었을 때 갱신하거나 등 입니다.

추가로 SWR의 옵션에 대한 자세한 내용은 아래 공식 문서 링크를 참고하기 바랍니다.

https://swr.vercel.app/docs/api#options

NOTE. 캐싱 여부를 어떻게 판단할까?
SWR에서는 useSWR()훅의 첫 번째 파라미터인 API 주소 또는 API 주소와 파라미터 조합으로 키(key)를 생성합니다.
그리고 캐시에 데이터를 저장할 때는 이 키(key)에 대한 값(value)으로 저장하게 됩니다.
동일한 API 요청에 대해서는 키(key)도 동일하기 때문에 이 값으로 캐시된 데이터를 찾을 수 있는 것입니다.
아래 SWR 개발자 도구의 Cache 탭을 참고하면 이해하는데 도움이 될 겁니다 😀

SWR Devtools

재사용 가능한 커스텀 훅으로 만들기

동일한 API에 대한 요청이라면 그것 조차도 반복되는 코드를 줄이기 위해 아래와 같이 커스텀 훅으로 만들어서 사용할 수 있습니다.

여기에서는 사용자 정보를 받아오는 API를 SWR을 사용해서 useUser()라는 커스텀 훅으로 만들었습니다.

import useSWR from 'swr';

function useUser() {
    const { isLoading, data, error } = useSWR('/api/user', fetcher)
 
    return {
        isLoading,
        user: data,
        isError: error
    }
}

function Avatar() {
    const { isLoading, user, error } =  useUser();  // 커스텀 훅 호출

    if (!isLoading) {
        return <Spinner />;
    }

    if (error) {
        return <div>에러 발생!</div>
    }

    return <img src={user.avatar} alt={user.name} />
}

이렇게 하면 어떤 컴포넌트에서든지 사용자 정보가 필요할 때마다 useUser() 훅을 호출해서 사용하면 됩니다.

이 때 SWR 같은 라이브러리를 처음 사용하는 분들이 아래와 같이 헷갈려 하는 경우가 있습니다.

결국 useUser()훅을 호출할 때마다 API 호출이 발생하는 것 아닌가요? 🤔

하지만 위에서 설명한 것처럼 서버로부터 가져온 데이터를 캐싱해놓기 때문에,

훅을 호출한다고 해서 매번 API 호출이 발생하는 것은 아닙니다.

그래서 Data Fetching의 개념과 SWR이 해주는 캐싱의 역할에 대해 잘 이해하고 사용하는 것이 필요합니다.

SWR에서 가져온 데이터 갱신하기

그렇다면 SWR에 캐싱된 데이터를 직접 갱신하려면 어떻게 해야할까요?

SWR에서는 데이터를 갱신 하는 것을 mutate라고 부르며, 관련된 다양한 기능들을 제공합니다.

먼저 아래와 같이 전역 mutator를 가져와서 특정 key에 대한 데이터를 갱신하는 Global Mutate 방법이 있습니다.

import { useSWRConfig } from "swr";
 
function App() {
    const { mutate } = useSWRConfig();
    mutate(key, data, options);
}
import { mutate } from "swr";
 
function App() {
    mutate(key, data, options);
}

그리고 아래와 같이 우리가 만든 커스텀 훅에서 제공하는 mutate() 함수를 사용할 수도 있습니다.

이 방법은 Bound Mutate라고 부르는데, 이미 키 값이 매핑되어 있기 때문에 그냥 호출하기만 하면 해당 API에 대해 갱신이 이뤄집니다.

import useSWR from 'swr';

function useUser() {
    const { isLoading, data, error, mutate } = useSWR('/api/user', fetcher)
 
    return {
        isLoading,
        user: data,
        isError: error,
        mutate
    }
}

function Content() {
    const { isLoading, user, error, mutate } =  useUser();

    if (!isLoading) {
        return <Spinner />;
    }

    if (error) {
        return <div>에러 발생!</div>
    }

    return (
        <div>
            <h1>안녕하세요, {user.name}</h1>
            /* 버튼을 누르면 갱신 됨 */
            <button onClick={mutate}>갱신하기</button>
        </div>
    )
}

SWR에서는 이러한 다양한 방법으로 캐싱된 데이터를 직접 갱신할 수 있습니다.


지금까지 Date Fetching과 SWR에 대해서 알아보았습니다.

저는 개인적으로 SWR을 굉장히 만족하면서 사용하고 있습니다.

사실 react-query와 비교했을 때 어느 것이 월등하게 더 좋다 라고 할 수는 없기 때문에,

두 가지 라이브러리 모두 실제 프로젝트에서 직접 사용해보면서 나에게 더 잘 맞는 것을 찾으시면 좋을 것 같습니다.

아래에 SWR 관련 링크들이 나와 있으니, 꼭 한 번 사용해보시기 바랍니다!

🔗 SWR 관련 링크


그럼 저는 다음에 또 유익한 글로 찾아뵙겠습니다!

지금까지 소플이었습니다. 감사합니다 😀

지금 가입하고 새로운 매거진을 이메일로 받아보세요!

Copyright ⓒ Soaple. All rights reserved.