Refreshing and Refetching (part. 2)
ํ๋๊ทธ๋จผํธ ๋ฆฌํ๋ ์ฑ
โrefreshing a fragmentโ์ด๋ผ๋ ๊ฒ์, ๊ธฐ์กด์ ํ๋๊ทธ๋จผํธ๋ก ๋ ๋ํ๋ ๋ฐ์ดํฐ์ ๋๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ํจ์นญํ์ฌ ํด๋น ๋ฐ์ดํฐ์ ๊ฐ์ฅ ์ต์ ๋ฒ์ ์ ์๋ฒ๋ก๋ถํฐ ๋ฐ์์ค๋ ๊ฒ์ ์๋ฏธํ๋ค.
real-time ํน์ง ์ด์ฉํ๊ธฐ
์๋ฒ๋ก๋ถํฐ ๊ฐ์ฅ ์ต์ ๋ฒ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๊ธฐ ์ํด์ ์ฒซ๋ฒ์งธ๋ก ํ ์ผ์, real-time ํผ์ณ๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด ์ ์ ํ์ง ํ๋จํ๋ ๊ฒ์ด๋ค. ์ด๋ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ์๋์ผ๋ก ๋ฆฌํ๋ ์ ํ ํ์ ์์ด, ์๋์ ์ผ๋ก ์ต์ ๋ฒ์ ์ด ์ ์ง๋๋๋ก ํ๋ค.
real-time ๊ธฐ๋ฅ์ ์์๋ก๋ GraphQL Subscription์ด ์๋๋ฐ, ์ด๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ์๋ฒ์ ๋คํธ์ํฌ ๊ณ์ธต์ ์ถ๊ฐ์ ์ธ configuration์ด ํ์ํ๋ค.
useRefetchableFragment
useRefetchableFragmentํ๋๊ทธ๋จผํธ์ ๋ฐ์ดํฐ๋ฅผ ์๋์ ์ผ๋ก ๋ฆฌํ๋ ์ํ๊ธฐ ์ํด์๋, ํ๋๊ทธ๋จผํธ๋ฅผ ํฌํจํ ์ฟผ๋ฆฌ๋ฅผ ๋ฆฌํจ์นํด์ผ ํ๋ค. ์ฃผ์ํ ์ ์, ํ๋๊ทธ๋จผํธ๋ ์ฟผ๋ฆฌ ์์ด ๊ทธ ์์ฒด๋ก ํจ์น๋ ์ ์๋ค. ๋ฐ๋์ ์ฟผ๋ฆฌ์ ์ผ๋ถ๋ก์จ ์กด์ฌํด์ผ ํ๊ธฐ ๋๋ฌธ์, ์์ฝ๊ฒ ํ๋๊ทธ๋จผํธ ์์ฒด๋ฅผ โํจ์น"ํ ์ ์๋ค.
ํ๋๊ทธ๋จผํธ ๋ฆฌํ๋ ์๋ฅผ ์ํด์๋, useRefetchableFragment ํ
๊ณผ @refetchable ๋๋ ํฐ๋ธ๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋๋ค. ์ด๋ ํ๋๊ทธ๋จผํธ๋ฅผ ๋ฆฌํจ์นํ๊ธฐ ์ํ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์์ฑํ์ฌ refetch ๋ฅผ ์ฌ์ฉํด ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ ์ ์๋๋ก ํ๋ค.
import type {UserComponent_user$key} from 'UserComponent_user.graphql';
// @refetchable ์ ๋ฐ๋ผ ๋ฆด๋ ์ด๊ฐ ์๋ ์์ฑํด์ค ํ์
import type {UserComponentRefreshQuery} from 'UserComponentRefreshQuery.graphql';
type Props = {
user: UserComponent_user$key,
};
function UserComponent(props: Props) {
const [data, refetch] = useRefetchableFragment<UserComponentRefreshQuery, _>(
graphql`
fragment UserComponent_user on User
# @refetchable ๊ฐ ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ๊ธฐ ์ํด ๋ฆด๋ ์ด๊ฐ ์ฟผ๋ฆฌ๋ฅผ ์๋ ์์ฑํด์ค๋ค.
@refetchable(queryName: "UserComponentRefreshQuery") {
id
name
friends {
count
}
}
`,
props.user,
);
return (
<>
<h1>{data.name}</h1>
<div>Friends count: {data.friends?.count}</div>
<Button
onClick={() => refresh()}>
Fetch latest count
</Button>
</>
);
} const refresh = useCallback(() => {
// ๋น ๋ณ์ `{}`๋ก ๋ฆฌํจ์น ํ๋ ๊ฒฝ์ฐ;
// ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ๋ ๊ธฐ์กด์ ๋ณ์๋ฅผ ์ฌ์ฉํ์ฌ @refetchable ๋ฅผ ์ฌ์ฉํ ์ฟผ๋ฆฌ๋ฅผ ๋ฆฌํจ์น ํ ์ ์๊ณ ,
// ๋ฐ๋ผ์ ๊ฐ์ฅ ์ต์ ์ ํจ์น๋ ๋ฐ์ดํฐ๋ก ์ปดํฌ๋ํธ๋ฅผ ์
๋ฐ์ดํธ ํ ์ ์๋ค.
// 'network-only' fetchPolicy๋ ํญ์ ์๋ฒ๋ก๋ถํฐ ํจ์นํจ์ ๋ณด์ฅํ๊ณ ๋ก์ปฌ ๋ฐ์ดํฐ ์บ์๋ฅผ ์ฌ์ฉํ์ง ์๋๋ก ํ๋ค.
refetch({}, {fetchPolicy: 'network-only'})
}), [/* ... */];useRefetchableFragment๋useFragment์ ๋น์ทํ๊ฒ ์๋ํ๋๋ฐ, ๋ค์๊ณผ ๊ฐ์ ์ ์ด ์ถ๊ฐ๋๋ค.@refetchable๋๋ ํฐ๋ธ๋ก ํ์๋ ํ๋๊ทธ๋จผํธ๋ฅผ ์์ํ๋ค.@refetchable๋๋ ํฐ๋ธ๋ โ๋ฆฌํจ์น ๊ฐ๋ฅํโ ํ๋๊ทธ๋จผํธ์๋ง ์ฌ์ฉ ๊ฐ๋ฅํ๋ฐ, ์ด๋Viewer/Query/Node๋ฅผ ์์๋ฐ๋ ๋ชจ๋ ํ์ (idํ๋๋ฅผ ๊ฐ์ง๊ณ ์๋ ํ์ ) ์ ์กด์ฌํ๋ ํ๋๊ทธ๋จผํธ๋ฅผ ์๋ฏธํ๋ค.
์ด๋
refetchํจ์๋ฅผ ๋ฆฌํดํ๋๋ฐ,Flow-type ๋์ด ์์ฑ๋ ์ฟผ๋ฆฌ๊ฐ ์์ํ๋ ์ฟผ๋ฆฌ ๋ณ์๋ฅผ ์์ธกํ๋ค.
๋ ๊ฐ์ Flow ํ์ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ์: ์์ฑ๋ ์ฟผ๋ฆฌ์ ํ์ (e.g.
UserComponentRefreshQuery) ๊ณผ ์ด๋ฏธ ์ถ๋ก ๊ฐ๋ฅํ ๋๋ฒ์งธ ํ์ ์ ์ ๊ณตํ์ฌ_๋ฅผ ์ ๋ฌํด๋ ๋๋ค.
refetch๋ 2๊ฐ์ง ์ฃผ์ input๊ณผ ํจ๊ป ํธ์ถํ๋ค.์ฒซ๋ฒ์งธ ์ธ์๋ ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ๊ธฐ ์ํ ๋ณ์ ์ธํธ์ด๋ค. ์์ ๊ฒฝ์ฐ,
refetch๋ฅผ ํธ์ถํ๊ณ ๋น ๋ณ์ ์ธํธ๋ฅผ ์ ๋ฌํ๋ฉด ์ฒ์ ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ ๋ ์ฌ์ฉํ ๋๊ฐ์ ๋ณ์๋ฅผ ๋ค์ ์ฌ์ฉํ์ฌ ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ๊ฒ ๋๋ฏ๋ก, ๋ฆฌํ๋ ์๋ก์จ ์๋ํ๋ค.๋๋ฒ์งธ ์ธ์๋
'network-only'fetchPolicy๋ฅผ ์ ๋ฌํ์ฌ ๋คํธ์ํฌ๋ก๋ถํฐ ํจ์นํ๊ณ ๋ก์ปฌ ๋ฐ์ดํฐ ์บ์๋ฅผ ์ฌ์ฉํ์ง ์์์ ๋ณด์ฅํ๋ค.
refetch๋ฅผ ํธ์ถํ๋ฉด ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌ๋ ๋๋งํ๊ณ , ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ๋fetchPolicy์ ๋ฐ๋ผ ๋คํธ์ํฌ ์์ฒญ์ด ๋ฐ์ํ๋ฏ๋กuseRefetchableFragment๊ฐ suspend ๋๋ค. ์ด๋ ์ฆ, suspend ๋ ๋ fallback ๋ก๋ฉ ์ํ๋ฅผ ๋ํ๋ด๊ธฐ ์ํด ํด๋น ์ปดํฌ๋ํธ๋ฅผSuspense๋ฐ์ด๋๋ฆฌ๋ก ๊ฐ์ธ์ผ ํจ์ ๋ดํฌํ๋ค.
Suspense ์ฌ์ฉ์ ํผํด์ผ ํ ๋
๋ช๋ช์ ๊ฒฝ์ฐ, ์ด๋ฏธ ๋ ๋๋ ์ฝํ
์ธ ๋ฅผ ์จ๊ธฐ๋ Suspense fallback์ ๋ณด์ฌ์ฃผ์ง ์์์ผ ํ ๋๊ฐ ์๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ, fetchQuery ๋ฅผ ์ฌ์ฉํ์ฌ ๋ก๋ฉ ์ํ๋ฅผ ์๋์ ์ผ๋ก ์ถ์ ํ ์ ์๋ค.
import type {UserComponent_user$key} from 'UserComponent_user.graphql';
// @refetchable์ ๋ฐ๋ผ ๋ฆด๋ ์ด๊ฐ ์๋ ์์ฑํ ํ์
import type {UserComponentRefreshQuery} from 'UserComponentRefreshQuery.graphql';
type Props = {
user: UserComponent_user$key,
};
function UserComponent(props: Props) {
const [data, refetch] = useRefetchableFragment<UserComponentRefreshQuery, _>(
graphql`
fragment UserComponent_user on User
# @refetchable์ ์๋์ ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นญํ๊ธฐ ์ํ ์ฟผ๋ฆฌ๋ฅผ ์๋ ์์ฑํ๋ค.
@refetchable(queryName: "UserComponentRefreshQuery") {
id
name
friends {
count
}
}
`,
props.user,
);
return (
<>
<h1>{data.name}</h1>
<div>Friends count: {data.friends?.count}</div>
<Button
disabled={isRefreshing}
onClick={() => refresh()}>
Fetch latest count {isRefreshing ? <LoadingSpinner /> : null}
</Button>
</>
);
} const [isRefreshing, setIsRefreshing] = useState(false);
const refresh = useCallback(() => {
if (isRefreshing) { return; }
setIsRefreshing(true);
// fetchQuery๋ ์ฟผ๋ฆฌ๋ฅผ ํจ์นํ๊ณ ๋ฆด๋ ์ด ์คํ ์ด์ ๋ฐ์ดํฐ๋ฅผ ์ด๋ค.
// ๋ฐ๋ผ์ ๋ฆฌ๋ ๋๋ง ์, ๋ฐ์ดํฐ๊ฐ ์ด๋ฏธ ์บ์๋์ด suspend ํ ํ์๊ฐ ์๋ค.
fetchQuery(environment, AppQuery, variables)
.subscribe({
complete: () => {
setIsRefreshing(false);
// ์ฟผ๋ฆฌ๊ฐ ํจ์น๋ **์ดํ**์,
// refetch๋ฅผ ๋ค์ ํธ์ถํ์ฌ ์
๋ฐ์ดํธ๋ ๋ฐ์ดํฐ๋ก ๋ฆฌ๋ ๋๋งํ๋ค.
// ์ฟผ๋ฆฌ๋ฅผ ์ํ ๋ฐ์ดํฐ๋ ์บ์๋์ด์ผ ํ๋ฏ๋ก,
// 'store-only' fetchPolicy๋ฅผ ์ฌ์ฉํ์ฌ suspending์ ๋ฐฉ์งํ๋ค.
refetch({}, {fetchPolicy: 'store-only'});
},
error: () => {
setIsRefreshing(false);
}
});
}, [/* ... */]);์ด์ ๋ฆฌํ๋ ์ฑ ํ ๋, suspending์ ํผํ๊ธฐ ๋๋ฌธ์ ์ฐ๋ฆฌ๋ง์
isRefreshing๋ก๋ฉ ์ํ๋ฅผ ์ถ์ ํ ์ ์๋ค. ์ด ์ํ๋ฅผ ์ฌ์ฉํ์ฌ ์ฝํ ์ธ ์จ๊น ์์ด ์ปดํฌ๋ํธ ๋ด busy ์คํผ๋๋ ๋ก๋ฉ UI๋ฅผ ๋ ๋๋ง ํ ์ ์๋ค.์ด๋ฒคํธ ํธ๋ค๋ฌ์์, ๋จผ์
fetchQuery๋ฅผ ํธ์ถํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ํจ์นํ๊ณ ๋ก์ปฌ ๋ฆด๋ ์ด ์คํ ์ด์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ค.fetchQuery๋คํธ์ํฌ ์์ฒญ์ด ๋๋๋ฉด,refetch๋ฅผ ํธ์ถํ์ฌ ์ด์ ์ ์์์ ๋น์ทํ๊ฒ ์ ๋ฐ์ดํธ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ ๋๋งํ๋๋ก ํ๋ค.์ด ์ ์์
refetch๊ฐ ํธ์ถ๋๋ฉด, ํ๋๊ทธ๋จผํธ๋ฅผ ์ํ ๋ฐ์ดํฐ๋ ๋ก์ปฌ ๋ฆด๋ ์ด ์คํ ์ด์ ์บ์๋์ด ์์ด์ผ ํ๋ค. ์ด๋ฅผ ํตํดstore-onlyfetchPolicy๋ฅผ ์ฌ์ฉํ์ฌ suspending์ ํผํ๊ณ ์ด๋ฏธ ์บ์๋ ๋ฐ์ดํฐ๋ง ์ฝ์ ์ ์๋ค.
๋ค๋ฅธ ๋ฐ์ดํฐ๋ก ํ๋๊ทธ๋จผํธ ๋ฆฌํจ์นญํ๊ธฐ
โrefetching a fragmentโ๋ ์ด๋ฏธ ํน์ ํ๋๊ทธ๋จผํธ๋ก ๋ ๋๋ง๋ ๋ฐ์ดํฐ์ ๋ค๋ฅธ ๋ฒ์ ์ ๋ฐ์ดํฐ๋ฅผ ํจ์นญํ๋ ๊ฒ์ ์๋ฏธํ๋ค. ์๋ฅผ ๋ค์ด, ํ์ฌ ์ ํ๋ ์์ดํ ์ ๋ณ๊ฒฝํ ๋, ์ด๋ฏธ ๋ณด์ฌ์ง๋ ์์ดํ ๋ฆฌ์คํธ์ ๋ค๋ฅธ ์์ดํ ๋ค์ ๋ ๋๋ง ํ ๋, ํน์ ๋ ์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ ํ์ฌ ๋ ๋๋ง๋ ์ฝํ ์ธ ์์ ์๋กญ๊ฑฐ๋ ๋ค๋ฅธ ์ฝํ ์ธ ๋ฅผ ๋ ๋๋ง ํ๋ ์ํ๋ก ๋ณํํ ๋ ํ๋๊ทธ๋จผํธ๋ฅผ ๋ฆฌํจ์นญ ํ๋ค.
๊ฐ๋ ์ ์ผ๋ก, ์ด๋ ํ์ฌ ๋ ๋๋ง๋ ํ๋๊ทธ๋จผํธ๋ฅผ ๋ค์ ํจ์นญ๊ณผ ๋ ๋๋งํ๋ ๊ฒ์ ์๋ฏธํ์ง๋ง, ๋ค๋ฅธ ๋ณ์๋ฅผ ์ฌ์ฉํ ์๋ก์ด ์ฟผ๋ฆฌ ๋๋ ์๋ก์ด ์ฟผ๋ฆฌ ๋ฃจํธ์์ ํ๋๊ทธ๋จผํธ๋ฅผ ๋ ๋๋ง ํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
useRefetchableFragment
useRefetchableFragmentimport type {CommentBodyRefetchQuery} from 'CommentBodyRefetchQuery.graphql';
import type {CommentBody_comment$key} from 'CommentBody_comment.graphql';
type Props = {
comment: CommentBody_comment$key,
};
function CommentBody(props: Props) {
const [data, refetch] = useRefetchableFragment<CommentBodyRefetchQuery, _>(
graphql`
fragment CommentBody_comment on Comment
@refetchable(queryName: "CommentBodyRefetchQuery") {
body(lang: $lang) {
text
}
}
`,
props.comment,
);
return (
<>
<p>{data.body?.text}</p>
<Button
onClick={() => refetchTranslation()}>
Translate Comment
</Button>
</>
);
}const refetchTranslation = () => {
// ์๋ก์ด ๋ณ์๋ฅผ ์ด์ฉํด refetch๋ฅผ ํธ์ถํ๋ฉด
// @refetchable ์ฟผ๋ฆฌ๋ฅผ ๊ทธ ์๋ก์ด ๋ณ์๋ค์ ์ด์ฉํด ๋ฆฌํจ์นํ๊ณ
// ๊ฐ์ฅ ์ต์ ์ ํจ์น๋ ๋ฐ์ดํฐ๋ก ํด๋น ์ปดํฌ๋ํธ๋ฅผ ์
๋ฐ์ดํธ ํ๋ค.
refetch({lang: 'SPANISH'});
};refetch๋ 2๊ฐ์ง ์ฃผ์ input๊ณผ ํจ๊ป ํธ์ถํ๋ค.์ฒซ๋ฒ์งธ ์ธ์๋ ํ๋๊ทธ๋จผํธ๋ฅผ ํจ์นํ๊ธฐ ์ํ ๋ณ์ ์ธํธ์ด๋ค. ์์ ๊ฒฝ์ฐ,
refetch๋ฅผ ํธ์ถํ๊ณ ์๋ก์ด ๋ณ์ ์ธํธ๋ฅผ ์ ๋ฌํ๋ฉด ์๋กญ๊ฒ ์ ๋ฌ๋ ๋ณ์์ ํจ๊ป ํ๋๊ทธ๋จผํธ๋ฅผ ํด์นํ๋ค. ์ด ๋ ์ ๊ณตํด์ผ ํ๋ ๋ณ์๋@refetchable์ฟผ๋ฆฌ๊ฐ ์์ํ๋ ๋ณ์ subset ์ด๋ค. ํ๋๊ทธ๋จผํธ๊ฐidํ๋๋ฅผ ๊ฐ๊ณ ์๋ ๊ฒฝ์ฐ ์ฟผ๋ฆฌ๋id๋ฅผ ์๊ตฌํ๊ณ , ๋ค๋ฅธ ๋ณ์ ๋ํ ํ๋๊ทธ๋จผํธ์์ ์ฌ์ฉํ๋ ํ๋๋ฅผ ์๊ตฌํ๋ค.์์ ๊ฒฝ์ฐ ํ์ฌ ์ฝ๋ฉํธ
id์translationType๋ณ์๋ฅผ ์ํ ์๋ก์ด ๊ฐ์ ์ ๋ฌํ์ฌ ๋ฒ์ญ๋ ์ฝ๋ฉํธ ๋ด์ฉ์ ํจ์นํ๋ค.
์์ ๊ฒฝ์ฐ ๋๋ฒ์งธ ์ต์ ์ธ์๋ฅผ ์ ๋ฌํ์ง ์๋๋ฐ, ์ด๋ ๋ํดํธ
fetchPolicy์ธstore-or-network๋ฅผ ์ฌ์ฉํ์ฌ ํด๋น ํ๋๊ทธ๋จผํธ๋ฅผ ์ํ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์ด๋ฏธ ์บ์๋ ๊ฒฝ์ฐ ๋คํธ์ํฌ ์์ฒญ์ ์คํตํจ์ ์๋ฏธํ๋ค.
Suspense ์ฌ์ฉ์ ํผํด์ผ ํ ๋
import type {CommentBodyRefetchQuery} from 'CommentBodyRefetchQuery.graphql';
import type {CommentBody_comment$key} from 'CommentBody_comment.graphql';
type Props = {
comment: CommentBody_comment$key,
};
function CommentBody(props: Props) {
const [data, refetch] = useRefetchableFragment<CommentBodyRefetchQuery, _>(
graphql`
fragment CommentBody_comment on Comment
@refetchable(queryName: "CommentBodyRefetchQuery") {
body(lang: $lang) {
text
}
}
`,
props.comment,
);
return (
<>
<p>{data.body?.text}</p>
<Button
disabled={isRefetching}
onClick={() => refetchTranslation()}>
Translate Comment {isRefetching ? <LoadingSpinner /> : null}
</Button>
</>
);
} const [isRefetching, setIsRefreshing] = useState(false)
const refetchTranslation = () => {
if (isRefetching) { return; }
setIsRefreshing(true);
fetchQuery(environment, AppQuery, variables)
.subscribe({
complete: () => {
setIsRefreshing(false);
refetch({lang: 'SPANISH'}, {fetchPolicy: 'store-only'});
}
error: () => {
setIsRefreshing(false);
}
});
};Last updated