SWR은 비동기 작업을 도와주는 React Hooks 라이브러리이다.
Cache된 데이터를 보여주고, 데이터 요청을 보낸 후 새롭게 받은 데이터를 보여주는 SWR에 대해 알아보자.
Stale-While-Revaildate
: React Hooks library for data fetching으로 데이터를 얻는 GET에 특화된 훅이다.
useSWR은 한 번의 fetch한 원격 상태의 데이터를 내부적으로 캐시한다.
즉, 다른 component에서 동일한 상태를 사용하고자 하는 경우에 서버로 재요청을 하는 것이 아니라 이전에 캐시했던 상태를 그대로 사용한다.
서로 다른 component에서 동일한 상태를 공유할 수 있다
주어진 리소스의 복사본을 저장하고 있다가 해당 리소스를 요청할 때 복사본을 제공하는 기술이다.
서버에 도달하기 전에 브라우저가 요청을 중간에 가져가 캐시에 있는 정보로 리소스를 응답한다.
import axios from "axios";
function Todo() {
// 상태 정의
const [ data, setData ] = useState(null);
// mount 되었을때 실행
useEffect(() => {
( async() => {
// 서버로부터 가져와서 보여주기
const { data } = await axios.get("/api/todos");
setData(data);
})();
},[])
return <div> { data }</div>
}
위의 코드에서의 단점은 다음과 같다.
import useSWR from 'swr';
const fetcher = url => fetch(url).then(res => res.json());
// axios.get(url).then(res => res.data)
function Todo() {
const { data, error } = useSWR('/api/todos', fetcher, options)
// 밑에 자체가 에러처리!
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div> { data }</div>
}
앞서 살펴본 단점을 모두 보완하게 된다.
데이터 패치 상태 ( react 의 suspense )
전역적으로 데이터 관리하여 데이터 캐싱 기능이 가능하다. ( swr cache )
즉, 반복적인 호출에 따른 비용이 발생하지 않는다.
일정 시간마다 원격 데이터를 자동 동기화한다 👉 revalidate
수동 동기화가 가능하다 👉 mutate
yarn add swr
npm install swr
import useSWR from "swr";
const { data, error, isValidating, mutate } = useSWR( key, fetcher, options)
첫 번째 인자 👉 key
두 번째 인자 👉 fetcher
key를 파라미터로 받고, 응답이 정상적으로 이루어지면 respnse.data를 useSWR의 data로 사용한다.
세 번째 인자 👉 options
refreshInterval = 0
revaildateOnFocus = true
revalidateOnReconnect = true
data
fetcher 통신 전에는 undefined가 할당되어 있으며, 통신 후에는 응답의 결과 data가 저장된다.
error
fetcher의 error가 저장된다.
revalidate
요청 또는 요청 재검증을 통해 SWR에 정의된 API를 다시 요청하여 데이터를 검증한다.
mutate(datam shouldRevaildate)
캐시되는 데이터를 저장한다.
mutate("newData", false)
// data 값을 newData로 바꾸고, 2번째 옵션이 false면 서버로 재요청을 하지 않는다.
👇 axios로 만든 fetcher코드이다.
import axios from 'axios';
const fetcher = (url: string) =>
axios
.get(url, {
withCredentials: true,
})
.then((response) => response.data);
export default fetcher;
const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
key에는 API 명세서에 적힌 url 끝부분을 넣어준다.
import useSWR from 'swr'
function Profile () {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
// render data
return <div>hello {data.name}!</div>
}
👇 사용자의 정보를 받아오는 hook 코드를 만들어보자.
function useUser (id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error
}
}
아래와 같은 방법으로 사용이 가능하다.
function UserProfile({ id }) {
const { user, isLoading, isError } = useUser(id)
if (isLoading) return <Spinner />
if (isError) return <Error />
return <img src={user.avatar} />
}
// 페이지 컴포넌트
// useEffect로 첫 렌더링에 정보를 가져와서
// 아직 정보가 없다면 Spinner라는 로딩 페이지를 보여주고
// 정보가 들어오면 그에 맞는 자식 컴포넌트들을 보여줍니다.
function Page () {
const [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>Welcome back, {user.name}</h1>
}
function Avatar ({ user }) {
return <img src={user.avatar} alt={user.name} />
}
최상위 부모 컴포넌트에서 가져온 데이터를 유지하고 트리 아래의 자식 컴포넌트들을 props로 넘겨준다.
context를 사용해서 props 드릴링을 피할 수 있지만, 동적 콘텐츠 문제가 있을 수 있다. 더불어 페이지 콘텐츠 내 컴포넌트들은 동적일 수 있으며, 최상위 레벨 컴포넌트는 그 자식 컴포넌트가 필요로하는 데이터가 무엇인지 알 수 없을 수도 있다.
👇 그래서 SWR로 리팩토링을 진행하는 것이 필요하다.
function Page () {
return <div>
<Navbar />
<Content />
</div>
}
// 자식 컴포넌트
function Navbar () {
return <div>
...
<Avatar />
</div>
}
function Content () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <h1>Welcome back, {user.name}</h1>
}
function Avatar () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <img src={user.avatar} alt={user.name} />
}
페이지에서 데이터를 받아와서 내려주는 형태가 아니라 각 컴포넌트들이 데이터를 받아올 수 있게 된다.
동일한 SWR 키를 사용하며 그 요청이 자동으로 중복 제거, 캐시, 공유되므로, 단 한 번의 요청만 API로 전송된다는 것이 핵심이다.
SWRConfig Component의 자식 컴포넌트들에게 사용된 swr에 동일한 options을 부여하는 기능이다.
function App () {
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
}}
>
<Dashboard />
</SWRConfig>
)
}
어떤 state에 따라 api 요청을 제어하고자 하는 기능이다.
👇 key값이 null이거나 key 대신 함수를 사용할 경우에 false한 값을 반환받으면 fetch를 멈추도록 하는 것도 가능하다.
// state에 따라 key 값을 null로 만들면 fetch가 실행되지 않는다.
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher)
// key 함수가 falsy 값을 리턴하면 fetch가 실행되지 않는다.
const { data } = useSWR(() => shouldFetch ? "/api/data" : null, fetcher)
// user가 존재하지 않아서 user.id에 접근할 때 error가 발생해 fetch가 실행되지 않는다.
const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher)
useSWR("/api/user", () => fetcher("/api/user"))
useSWR("/api/user", url => fetcher(url))
useSWR("/api/user", fetcher)
useSWR("/api/user", url => fetchWithToken(url, token))
const { data : user } = useSWR(["/api/user", token], fetchWithToken)
useSWRConfig() hook으로부터 mutate 함수를 얻을 수 있으며, mutate(key)를 호출하여 동일한 키에 해당하는 전역의 모든 SWR을 revaildation한다.
import useSWR, { useSWRConfig } from "swr";
const App = () => {
const { mutate } = useSWRConfig();
return (
<div>
<Profile/>
<button onClick={() => {
// 쿠키를 만료된 것으로 설정
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
// 이 키로 모든 SWR에게 갱신하도록 요청
mutate('/api/user')
}}>
Logout
</button>
</div>
)
};
import useSWR, { useSWRConfig } from 'swr'
function Profile () {
const { mutate } = useSWRConfig()
const { data } = useSWR('/api/user', fetcher)
return (
<div>
<h1>My name is {data.name}.</h1>
<button onClick={async () => {
const newName = data.name.toUpperCase()
// 로컬 데이터를 즉시 업데이트하지만, 갱신은 비활성화
mutate('/api/user', { ...data, name: newName }, false)
// 소스 업데이트를 위해 API로 요청 전송
await requestUpdateUsername(newName)
// 로컬 데이터가 올바른지 확인하기 위해 갱신(refetch) 트리거
mutate('/api/user')
}}>Uppercase my name!</button>
</div>
)
}
👇 mutate 함수에 현재 캐시 된 값을 받는(있으면 업데이트된 문서를 반환) 비동기 함수를 전달
하면 update가 가능하다.
mutate('/api/todos', async todos => {
// ID `1`을 갖는 todo를 업데이트해 완료되도록 해봅시다
// 이 API는 업데이트된 데이터를 반환합니다
const updatedTodo = await fetch('/api/todos/1', {
method: 'PATCH',
body: JSON.stringify({ completed: true })
})
// 리스트를 필터링하고 업데이트된 항목을 반환합니다
const filteredTodos = todos.filter(todo => todo.id !== '1')
return [...filteredTodos, updatedTodo]
})
📚 학습할 때, 참고한 자료 📚