인생마커
article thumbnail
Published 2022. 2. 18. 01:10
SWR React/Next.js

개요

FLUX 패턴의 대표격인 Redux는 React와는 상관이 없고 바닐라 js에서도 사용할 수 있는 상태 관리 라이브러리지만 40%가 넘는 React 개발자가 사용하고 있습니다.

 

그럼에도 Redux는 꽤 말이 많습니다.

 

비동기 처리를 위한 미들웨어 사용과 익숙치 않은 동작원리 라던지, 상태 초기화와 동기화에서 유지관리해야할 코드도 많아집니다.

 

Props Drilling을 최소화 함으로써 복잡도를 낮추는데 의의가 있는 것인데, 오히려 복잡한 구조로 인해 복잡도가 올라갈 수 있고 이것에 대해 지적하는 개발자들이 생기기 시작합니다.

 

개발자는 장애물이 커지면 돌아갈 길을 모색하는 모양입니다.

 

SWR은 Next.js를 만든 Vercel 팀의 "데이터를 가져오기 위한 React Hooks" 입니다.

 

사실 SWR은 상태 관리를 위해 만들어진게 아니지만 SWR의 컨셉과 강력한 기능은 상태 관리에 대단히 효과적입니다.

 

사용

 

SWR을 사용하면 컴포넌트는 지속적이며 자동으로 데이터 업데이트 스트림을 받게 됩니다. 그리고 UI는 항상 빠르고 반응적입니다.

 

우선 설치해보도록 합시다.

 

npm install swr
yarn add swr

 

useUser.js

function useUser (id) {
  const fetcher = (...args) => fetch(...args).then(res => res.json())
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}

 

fetcher 함수는 GraphQL, axios와 같은 라이브러리를 사용할 수도 있습니다.

 

이렇게 작성한 후 커스텀 훅처럼 user에 대한 정보를 사용하고 싶은 컴포넌트에서 불러와서 사용하면 됩니다.

 

function Avatar ({ id }) {
  const { user, isLoading, isError } = useUser(id)

  if (isLoading) return <Spinner />
  if (isError) return <Error />
  return <img src={user.avatar} />
}

 

 

우리가 중앙 집중식 저장소(Redux, Recoil..)를 사용한다고 하면
서버의 데이터를 받아와서 store의 state에 데이터를 저장하고 그다음 페이지에 렌더링 되는 과정을 거쳐야 할 것인데
SWR은 그 중간 과정이 생략된다고 볼 수 있습니다.

 

서버에서 직접 데이터를 가지고 와서 마치 상태 관리 라이브러리를 사용하는 것처럼 만들어줍니다.

 

그리고 데이터가 필요한 컴포넌트들이 prop 전달 없이 훅을 불러오기만 하면 되기 때문에 전역 state를 사용하는 것 처럼 컴포넌트의 독립이 보장됩니다.

 

그리고 데이터의 삭제나 수정이 이뤄질 경우를 대비해서 Redux는 스토어의 상태를 서버와 동기화시켜줘야 합니다.

 

하지만 SWR 이런 과정도 필요 없습니다.

 

SWR은 로컬에서 사용하는 데이터가 최신 상태를 유지할 수 있도록 동기화 시점을 설정해놓고 페이지가 다시 포커스 되거나 탭을 전환하거나 할 때 데이터를 자동으로 fetch 합니다.

 

당연히 사용자가 fetch 되는 시점을 지정할 수도 있습니다.

 

그리고 SWR은 fetch된 서버 데이터를 내부적으로 캐싱합니다.

 

그리고 다른 컴포넌트에서 동일한 상태를 요구할 경우 캐싱된 데이터를 반환해줍니다.

 

그렇기 때문에 여러 컴포넌트에서 user의 데이터를 사용한다 하더라도 fetch는 한 번만 이루어집니다.

 

세 번째는 Next.js와 호환이 좋고 문서도 잘 정리되어 있습니다.

 

SWR은 Next.js를 만든 팀에서 만든 것이기 때문에, Next.js의 기능을 사용할 때 SWR을 많은 경우에 효과적으로 사용할 수 있습니다.

 

아래는 SWR 컨셉을 직관적으로 확인할 수 있게 작성했습니다.


예제

useUser.js

import useSWR from 'swr';
import axios from 'axios';

export default function useUser() {
  const fetcher = async url => {
    const resp = await axios.get(url);
    return resp.data;
  };
  const { data, mutate, error } = useSWR(
    `http://localhost:4000/users`,
    fetcher,
  );

  return {
    user: data,
    isLoading: !error && !data,
    isError: error,
    mutate,
  };
}

 

fetcher는 axios를 사용했고 JSON server로 mock 서버를 만들어서 요청을 보냈습니다.

 

index.js

import React, { useState } from 'react';
import useUser from '../swr/useUsers';
import styles from './index.module.scss';
import axios from 'axios';
const Index = () => {
  // useUser Hook 에서 return 되는 값 또는 함수를 분해 할당
  const { user, isLoading, isError, mutate } = useUser();
  // user 추가 POST 요청시 사용되는 local state
  const [addUser, setAddUser] = useState({
    name: '',
    gender: '',
  });
  // POST 요청 로직
  const post = async addUser => {
    // mutate는 요청 trigger이다.
    // 인자로 false를 주면 요청은 가지 않지만 로컬에 캐쉬된 값을 미리 변경해준다.
    // 변경해야 할 값을 로컬에서 알고 있으니 캐쉬된 값을 미리 변경해서 렌더속도를 최적화 하는 부분
    mutate([...user, addUser], false);
    // POST 요청
    const resp = await axios.post('http://localhost:4000/users', addUser);
    // 앞에서 미리 캐쉬된 값을 변경했지만 완전한 상태로 다시 저장하기 위해 한번 더 불러준다.
    mutate([...user, resp.data], false);
  };
  // DELETE
  const deleteBtn = async id => {
    await axios.delete(`http://localhost:4000/users/${id}`);
    mutate();
  };

  return (
    <>
      <div className="container">
        {isLoading
          ? `loading`
          : user.map((article, index) => {
              return (
                <div key={index}>
                  <div className={styles.res}>
                    <p>id: {article.id}</p>
                    <p>name: {article.name}</p>
                    <p>gender: {article.gender}</p>
                    <button onClick={() => deleteBtn(article.id)}>
                      delete
                    </button>
                  </div>
                </div>
              );
            })}
        <input
          type="text"
          onChange={e =>
            setAddUser({ ...addUser, name: e.currentTarget.value })
          }
        />
        <input
          type="text"
          onChange={e =>
            setAddUser({ ...addUser, gender: e.currentTarget.value })
          }
        />
        <button onClick={() => post(addUser)}>add</button>
      </div>
    </>
  );
};
export default Index;

 

서버에서 데이터 user에 대한 데이터를 받아오고 post와 delete 기능이 있습니다.

 

user를 하나 추가해보도록 합시다.

 

 

요청 중 하나는 mock server와 리액트 개발 서버의 포트 번호가 달라서 CORS 문제 때문에 생긴 요청입니다.

 

 

[TIL] `OPTIONS` 요청은 왜 발생하는가?

웹 개발을 하다 보면 네트워크 요청시 실제 원하는 요청(GET, PUT, POST, DELETE 등)전에 OPTIONS 요청이 발생하는 것을 볼 수 있다. 이게 뭘까하고 응답값을 확인하면 아무것도 없다. 응답값이 없는 이

nukeguys.github.io

 

결국 POST 요청 하나만 갔을 뿐인데 캐시를 업데이트시켜줬기 때문에 화면에 바로 반영되는 것입니다.

 

다른 기능을 보도록 합시다.

 

about 페이지에서도 user를 불러와서 렌더링 해줬습니다.

 

그리고 동기화 작업이라던지 별 다른 작업을 하지 않았지만 데이터 갱신이 이루어집니다.

 

페이지에 포커스 하거나 탭을 전환할 때, SWR은 자동으로 데이터를 갱신한다는 컨셉덕분입니다.

 

당연히 비활성화도 가능합니다.

 

refreshInterval 값을 설정하면 일정 시간마다 데이터를 갱신할 수도 있고 반대로 데이터가 캐시 되기만 하면 절대 갱신되지 않게 하는 속성도 있습니다.

profile

인생마커

@Cottonwood__

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!