ํ์ฌ ๋๋ฃ๋ค๊ณผ ํจ๊ป ์งํํ๊ณ ์๋ ์ฌ์ด๋ ํ๋ก์ ํธ์ React-query๋ฅผ ๋์ ํด๋ดค์ต๋๋ค. React-query๋ฅผ ์ ์ฌ์ฉํ๋์ง? ์ ๋ํด์ ์๊ฐํ๊ณ ์ฌ์ฉ ๋ฐฉ๋ฒ๊ณผ ํจ์จ์ ์ธ ๊ธฐ๋ฅ๋ค ์๊ฐ์ ํจ๊ป React-query๋ฅผ ์ด์ฉํ์ฌ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํด๋ณธ ๋ฐฉ๋ฒ์ ๊ธฐ๋กํ๊ณ ์ ํฉ๋๋ค. โญ๏ธ๋ง์ ๋์ ๋๊ธธ ๋ฐ๋๋๋คโญ๏ธ
ํ์ฌ๋ React-query
๊ฐ ๋ฒ์ 4๊ฐ ๋ฆด๋ฆฌ์ค ๋ ์ํ๊ณ , ์ด๋ฆ ๋ํ ๋ฐ๋์๋ค.
๋ฒ์ 4์์๋ React-query๊ฐ ์๋ TanStack Query๋ก ๋ถ๋ฆฐ๋ค. (๊ทธ๋๋ ๋ ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๋ผ ๋ถ๋ฅผ๊ฑฐ๋ค!)
@tanstack/react-query
๋ก ๊ฐ์ ธ์จ๋ค.import { useQuery } from '@tanstack/react-query';
3๋ฒ์ -> useQuery('todos', fetchTodos)
4๋ฒ์ -> useQuery(['todos'], fetchTodos)
@tanstack/react-query-devtools
๋ก ๊ฐ์ ธ์์ผํ๋ค. 3๋ฒ์ ์์๋ ๊ฐ๋ฐ์ ๋๊ตฌ ๋ณ๋๋ก ์ค์นํ ํ์๊ฐ ์๋ค.import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
npm install react-query
npm install react-query@^3 // ๋ฒ์ 3
App.tsx
๋๋ Next ๊ฒฝ์ฐ _app.tsx
import { QueryClient, QueryClientProvider } from "react-query"; //๐์ถ๊ฐ
const queryClient = new QueryClient();//๐์ถ๊ฐ
function App() {
return (
<QueryClientProvider client={queryClient}> //๐์ถ๊ฐ
<div className="App">
(...)
</div>
</QueryClientProvider>
);
}
export default App;
<QueryClientProvider> </QueryClientProvider>
๋ก ๊ฐ์ธ๊ธฐ! ๊ทธ๋ผ ์ธํ
๋์ด๋ค. ์์ ์ด๊ฐ๋จ
๋ฆฌ์กํธ์ฟผ๋ฆฌ ๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ ์ด์
๋ฒ์ 4์์ ๊ฐ๋ฐ์ ๋๊ตฌ ์ค์นํ ๋ ๋ช ๋ น์ด (๋ฒ์ 3์ ์ค์นํ ํ์ ์๋ค)
npm i @tanstack/react-query-devtools
or
yarn add @tanstack/react-query-devtools
App.tsx
๋๋ Next ๊ฒฝ์ฐ _app.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // ๐ ์ถ๊ฐ
return (
<>
<QueryClientProvider client={queryClient}>
(... ๋ค๋ฅธ ์ฝ๋๋ ์๋ต)
<ReactQueryDevtools /> // ๐ ์ถ๊ฐ
</QueryClientProvider>
</>
);
๊ฐ๋ฐ์ ๋๊ตฌ๊น์ง ์ธํ ์ด ์๋ฃ๋๋ฉด ๋ธ๋ผ์ฐ์ ์ ๊ฝ๋ชจ์ ์์ด์ฝ์ด ์๊ธฐ๋๋ฐ ํด๋ฆญํ๋ฉด ์๋์ ๊ฐ์ ํ๋ฉด์ด ๋์จ๋ค. ๊ทธ๋ฆฌ๊ณ devtools๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ๋ฐํ๊ฒฝ์์๋ง ์ ๊ณต๋๋ฏ๋ก ํ๋ก๋์ ๋น๋ ์ค์ ์ ์ธํ๋ ๊ฒ์ ๋ํด ๊ฑฑ์ ํ ํ์ ์๋ค.
const { data } = useQuery("์ฟผ๋ฆฌ๋ช
", ์ฟผ๋ฆฌํจ์ = ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์, ์ต์
);
const { data, isError, isLoading } = useQuery("posts", fetchPosts, {staleTime: 2000})
if(isLoading) return <h3>๋ก๋ฉ์ค...</h3>
fresh
์๋ค๊ฐ -> stale
๋ก ๋ณํ ๊ฒ์ ๋ณผ ์ ์๋ค.0ms
์ด๋๋ฉด, ๋ฐ์ดํฐ๋ ํญ์ ๋ง๋ฃ ์ํ์ด๋ฏ๋ก ์๋ฒ์์ ๋ค์ ๊ฐ์ ธ์์ผ ํ๋ค๊ณ ๊ฐ์ ํ๋ค๋ ๋ป์ด๋ค. staleTime
์๋์ฐ๊ฐ ๋ค์ ํฌ์ปค์ค๋ ๋ ๊ฐ์ ํน์ ํธ๋ฆฌ๊ฑฐ์์ ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๊ฐ์ ธ์ฌ์ง ๊ฒฐ์ ํ๋ค.์ ์ฉ๋๋์ง๋ฅผ ํ์ธํ๊ธฐ ์ํด์๋ ๊ฐ๋ฐ์๋๊ตฌ์์ 2์ด ๋ค์ ๋ฐ์ดํฐ๊ฐ ํจ์นญ๋๋ ๊ฒ์ ํ์ธํด๋ณผ ์ ์๋ค !
์บ์๋ ๋์ค์ ๋ค์ ํ์ํ ์๋ ์๋ ๋ฐ์ดํฐ์ฉ์ด๋ค.
cacheTime
์ ๋ฐ์ดํฐ๊ฐ ๋นํ์ฑํ๋ ์ดํ ๋จ์ ์๋ ์๊ฐ์ ๋งํ๋ค.
ํน์ ์ฟผ๋ฆฌ์ ๋ํ ํ์ฑ useQuery
๊ฐ ์๋ ๊ฒฝ์ฐ, ํด๋น ๋ฐ์ดํฐ๋ ์ฝ๋ ์คํ ๋ฆฌ์ง
๋ก ์ด๋ํ๋ค.
๐ ์ฝ๋ ์คํ ๋ฆฌ์ง๋? ๋จ๊ฒจ์ง ๋ฐ์ดํฐ๋ฅผ ๋งํจ
๊ตฌ์ฑ๋ cacheTime
์ด ์ง๋๋ฉด ์บ์์ ๋ฐ์ดํฐ๊ฐ ๋ง๋ฃ๋๋ฉฐ ์ ํจ ์๊ฐ์ ๊ธฐ๋ณธ๊ฐ์ 5๋ถ์ด๋ค.
์บ์๊ฐ ๋ง๋ฃ๋๋ฉด ๊ฐ๋น์ง ์ปฌ๋ ์
์ด ์คํ๋๊ณ ํด๋ผ์ด์ธํธ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
๋ฐ์ดํฐ๊ฐ ์บ์์ ์๋ ๋์์๋ ํ์นญํ ๋ ์ฌ์ฉ๋ ์ ์๋ค.
์๋ฒ์ ์ต์ ๋ฐ์ดํฐ๋ก ์๋ก ๊ณ ์นจ์ด ๊ฐ๋ฅํ๋ค.
const { data, isError, isLoading, error } = useQuery("posts", fetchPosts, {staleTime: 2000})
if(isLoading) return <h3>๋ก๋ฉ์ค...</h3>
if(isError) return <h3>์๋ชป๋ ๋ฐ์ดํฐ์
๋๋ค. {error.toString()}</h3>
useQuery์์ ์ฌ์ฉํ๊ณ ์ถ์ ๊ธฐ๋ฅ๋ค์ ๊ตฌ์กฐ ๋ถํด ํ ๋นํ์ฌ ์ฌ์ฉํ๋ฉด ๋๋ค.
์ด๊ฒ ์ธ์๋ ์ฌ๋ฌ ๊ธฐ๋ฅ๋ค์ด ์์ผ๋ ๊ณต์๋ฌธ์ ํ์ธํ๋ฉด ๋๋ค.
์ด๋ ํ ํธ๋ฆฌ๊ฑฐ๊ฐ ์์ด์ผ๋ง ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๊ฐ์ ธ์ค๊ฒ ๋๋ค.
๊ทธ ์ด๋ ํ ํธ๋ฆฌ๊ฑฐ๋ ? ๐
const { data, isLoading, isError, error } = useQuery(
["comments", post.id], () => fetchComments(post.id)
);
์ด์ฒ๋ผ ์์กด์ฑ ๋ฐฐ์ด๋ก ์ทจ๊ธํด์ ์ฌ์ฉํ๋ฉด ๋๋ค. comments
๋ฅผ ์ฟผ๋ฆฌํค๋ก ์ง์ ํ ๊ฒ์ด๋ค.
๊ทธ๋ฆฌ๊ณ post.id๊ฐ ์
๋ฐ์ดํธ ๋๋ฉด ๋ฆฌ์กํธ์ฟผ๋ฆฌ๊ฐ ์ ์ฟผ๋ฆฌ๋ฅผ ๋ง๋ค๊ณ staleTime
๊ณผ cacheTime
์ ๊ฐ๊ฒ ๋๋ค.
๋ง๋ฃ (stale)
์ํ๋ฅผ ๋ํ๋ธ๋ค.import { useQuery, ๐useQueryClient } from "react-query";
const queryClient = useQueryClient();
useEffect(() => {
// 10ํ์ด์ง์ ์๋ค๋ฉด ๋ฏธ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ ํ์๊ฐ ์๋ค.
if (currentPage < maxPostPage) {
const nextPage = currentPage + 1;
queryClient.prefetchQuery(["posts", nextPage], () =>
fetchPosts(nextPage)
);
}
}, [currentPage, queryClient]);
const { data, isError, isLoading, error } = useQuery(
// ๋ฐฐ์ด์ ๋ด๊ธด ์ฒซ ๋ฒ์งธ ์์๋ฅผ ์ฟผ๋ฆฌํค๋ผ๊ณ ํ๋ค.
["posts", currentPage],
// ์ด ๋ฐฐ์ด์ด ๋ฐ๋๋ฉด ํจ์๋ ๋ฐ๋๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ๊ฐ ๋ฐ๋ ์๋ฐ์ ์๋ค.
() => fetchPosts(currentPage), // -> ํจ์์ ํ๋ผ๋ฏธํฐ๊ฐ์ currentPage๋ก ํจ
{
staleTime: 2000,
// ์ฟผ๋ฆฌํค๊ฐ ๋ฐ๊ปด๋ ์ง๋ ๋ฐ์ดํฐ๋ฅผ ์ ์งํด์ ์ด์ ํ์ด์ง๋ก ๋์๊ฐ์ ๋ ์บ์์ ํด๋น ๋ฐ์ดํฐ๊ฐ ์๋๋ก ํด์ค๋ค.
keepPreviousData: true,
}
);
Dev tools๋ฅผ ํ์ธํด๋ณด๋ฉด "posts", 9
๊ฐ "posts", 8
๋ณด๋ค ๋ ๋ฏธ๋ฆฌ ํจ์นญ ๋ ๊ฒ์ ๋ณด์ ๋ฏธ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ fetch ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ์ด๊ฒ์ด ๋ฐ๋ก ํ๋ฆฌํจ์นญ์ด๋ผ๊ณ ํ๋ค.
import { useQuery, ๐ useMutation, useQueryClient } from "react-query";
const deleteMutation = useMutation((postId) => deletePost(postId));
const updateMutation = useMutation((postId) => updatePost(postId));
<button onClick={() => deleteMutation.mutate(post.id)}>์ญ์ </button>
<button onClick={() => updateMutation.mutate(post.id)}>์
๋ฐ์ดํธ</button>
import { useMutation } from '@tanstack/react-query';
// ๋๊ธ ์ญ์
const { mutate } = useMutation((id: any) => deleteComment(id));
const handleCommentDelete = () => {
confirm('๋๊ธ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?');
mutate(userId, {
// ๋ฐ์ดํฐ ์์ฒญ์ ์ฑ๊ณตํ์ ๊ฒฝ์ฐ
onSuccess: () => {
alert('์ญ์ ์๋ฃ๋์์ต๋๋ค.');
},
});
};
useInfiniteQuery(['์ฟผ๋ฆฌ๋ช
'], ({ pageParam = defaultUrl}) => ๋ฐ์ดํฐํจ์(pageParam))
const { โญ๏ธ data, fetchNextPage, hasNextPage, isLoading, isError } = useInfiniteQuery(
['page'],
({ pageParam = 0 }) => getFeedPost({ page: pageParam, content: searchText, view: 5 }),
{
getNextPageParam: (lastPage, allPosts) => {
return lastPage.page !== allPosts[0].totalPage ? lastPage.page + 1 : undefined;
},
},
);
(1) pages: ๋ฐ์ดํฐ์ ํด๋น
(2) pageParams: ์ด๊ฒ์ ๊ฐ ํ์ด์ง์ ์ฟผ๋ฆฌ ํจ์์ ์ ๋ฌ๋๋ ๋งค๊ฐ๋ณ์์ด๋ค.
๋ฐฑ์๋ ๋ถ์ด ์์ ํด์ฃผ์ ํผ๋ ๊ฒ์๋ฌผ api ๋ฐ์ดํฐ๋ค. ๋ณด๋ค์ํผ ํ๋ผ๋ฏธํฐ ๊ฐ์ page๋ totalPage์ view๊ฐ ์๋ค.
page
: ํ์ด์ง ์๋ฅผ ์๋ฏธtotalPage
: ์ด ํ์ด์ง ์๋ฅผ ์๋ฏธview
: ํ ํ์ด์ง์ ๋ช๊ฐ์ ๊ฒ์๋ฌผ์ ๋ณด์ฌ์ค ์ง๊ทธ๋์ ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํ ํจ์นญ ํจ์
๋ฅผ ์์ฑํ๊ณ page ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์ pageParams
๋ก ์ ์ฉํ๊ณ , ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์์ ์ ๊ณตํด์ฃผ๋ ํจ์๋ค๋ก ์ ์ดํ์ฌ ์คํฌ๋กค ์์น๊ฐ ํ์ด์ง ํ๋จ์ ๋ง๋ฟ์์ ๋๋ง๋ค page๊ฐ +1์ด ๋๋ฉด์ ๋ค์ ํ์ด์ง์ ๋ฐ์ดํฐ๋ค์ด ์คํฌ๋กค ํ ๋๋ง๋ค ์๋กญ๊ฒ ๋ถ๋ฌ์์ ธ ๋ณด์ฌ์ง๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ์คํฌ๋กค ์์น๊ฐ ํ์ด์ง ํ๋จ์ ๋ง๋ฟ์์ ๋๋ง๋ค ์ง์ ์ฝ๋๋ฅผ ๊ตฌํํด๋ ์ข์ง๋ง ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํ์ฌ ์ ๋ง ๊ฐํธํ๊ฒ ๊ตฌํํ๋ค. ๋ฐ๋ก react-infinite-scroller
๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํ์ฌ ์์
ํ๋ค.
(์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฐ๋กํ๋ง๋ ๋ด์ฌ๋์ด ์๋ค.)
์ฐ๋กํ๋ง์ด๋? ๋ง์ง๋ง ํจ์๊ฐ ํธ์ถ๋ ํ ์ผ์ ์๊ฐ์ด ์ง๋๊ธฐ ์ ์ ๋ค์ ํธ์ถ๋์ง ์๋๋ก ํ๋ ๊ฒ.
์ฐ๋กํ๋ง์ ์คํฌ๋กค์ ์ฌ๋ฆฌ๊ฑฐ๋ ๋ด๋ฆด ๋ ๋ณดํต ์ฌ์ฉํฉ๋๋ค !
// ์ค์น ๋ช
๋ น์ด
npm install react-infinite-scroller
ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ์ค๋ช ์ ์ฐธ๊ณ
https://www.npmjs.com/package/react-infinite-scroller
import { useInfiniteQuery } from '@tanstack/react-query';
import InfiniteScroll from 'react-infinite-scroller';
const home = () => {
// ํผ๋ API ๋ถ๋ฌ์ค๋ ํจ์
const getFeedPost = async ({ page, content, view, tag }: IDetailPost) => {
const res = await Axios.get(`/post`, {
params: {
page,
view,
content,
tag,
},
});
return res.data.data;
};
// ๋ฐ์ดํฐ ํจ์นญ
const { data, fetchNextPage, hasNextPage, isLoading, isError } = useInfiniteQuery(
['page', search],
({ pageParam = 0 }) => getFeedPost({ page: pageParam, content: search, view: 5 }),
{
getNextPageParam: (lastPage, allPosts) => {
return lastPage.page !== allPosts[0].totalPage ? lastPage.page + 1 : undefined;
},
},
);
if (isLoading) return <h3>๋ก๋ฉ์ค</h3>;
if (isError) return <h3>์๋ชป๋ ๋ฐ์ดํฐ ์
๋๋ค.</h3>;
return (
<main>
{/* ํผ๋ ๊ฒ์๋ฌผ */}
<InfiniteScroll hasMore={hasNextPage} loadMore={() => fetchNextPage()}>
<FeedItem data={data} />}
</InfiniteScroll>
</main>
);
};
export default home;
<InfiniteScroll>
๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ loadMore์ ์ ์ฉ์ํค๋ฉด, ๋ ๋ถ๋ฌ์ฌ ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ง๋ค ์์์ ๋ก๋ํด์ค๋ค.lastPage
์ allPosts
๋ฅผ ์ฌ์ฉํ ์ ์๋ค. lastpage๋ ๋ค์ ํ์ด์ง๋ฅผ ์๋ฏธํ๊ณ , allPosts๋ ์ด ํฌ์คํธ ์๋ฅผ ์๋ฏธํ๋ค.getNextPageParam: (lastPage, allPosts) => {
return lastPage.page !== allPosts[0].totalPage ? lastPage.page + 1 : undefined;
},
์คํฌ๋กค์ ํ ๋๋ง๋ค ํ์ฌ ํ์ด์ง๊ฐ allPosts(์ด ํ์ด์ง ์)์ ๊ฐ์ง ์๋ค๋ฉด page + 1์ ํด์ฃผ์ด ์๋ก์ด ํ์ด์ง๋ฅผ ๊ณ์ํด์ ๋ถ๋ฌ์ค๊ณ , ํ์ฌ ํ์ด์ง์ allPosts์ ํ์ด์ง ์๊ฐ ๊ฐ๋ค๋ฉด undefined
๋ฅผ ํ์ฌ ๋ฐ์ดํฐ ํธ์ถ์ด ๋ฉ์ถฐ์ง๋๋ก ํ๋ค.
const FeedItem = ({ data }: any) => {
return (
<div css={FeedItemStyle}>
{data.pages.map((page: PageInfo) => {
return page.list.map((item: FeedItemProps) => {
return (
<div className="feedLayout" key={item.id}>
<div className="feedLayout__bg">
<ItemHeader
nickname={item.writer?.nickname}
mbti={item.writer?.mbti}
level={item.writer?.level}
profileImage={item.writer?.profileImage}
createAt={item.createAt}
/>
<Link href={`/home/${item.id}`}>
<ItemContent
id={item.id}
title={item.title}
content={item.content}
pollList={item.pollList && item.pollList}
tags={item.tags && item.tags}
/>
</Link>
<ItemFooter like={item.like?.count} command={item.command?.count} bookmark={item.bookmark?.count} />
</div>
</div>
);
});
})}
</div>
);
};
export default FeedItem;
์ฝ๋ ๊ธธ์ด์ง๋๊ฒ ์ซ์ด์ ์ปดํฌ๋ํธ๋ก ์ชผ๊ฐ๋ !
react-query์์ useInfiniteQuery
๋ก ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ ๊ฒฐ๊ณผ๋ฌผ์
๋๋ค.
์กฐ๋ง๊ฐ ๊ฐ์คํ์ผ๋ก ๋ฐฐํฌํ ์์ ....................
์ง์ง ์ง์ง ์ต์ข
์คํ์ 5์์ด๋ค ํ์ดํ
๐ฅณ
๋๊ธ ๋ณด์ค์ง ๋ชจ๋ฅด๊ฒ ๋๋ฐ.. ํ ๋ฒ ๋จ๊ฒจ๋ด
๋๋ค.
์ ๊ฐ ํ๋ก์ ํธ๋ฅผ ์งํ ์ค์ธ๋ฐ, react-query๋ก ๋ฌดํ ์คํฌ๋กค์ ์ ์ฉํ๋ค๊ฐ ์ต๊ทผ์ ๊ด๋ จ๋ ๋ถ๋ถ์ ๋ค์ ์์
ํ ์ผ์ด ์๊ฒผ์ต๋๋ค.
api ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ฉด re-rendering์ด ๋๋ ๊ฑด ๋น์ฐํ๋ฐ, ์ ์ด์ ์ ๋ฐ์์จ ๋ฐ์ดํฐ๋ค๊น์ง re-rendering์ด ๋๋์ง ํน์ ์ด๋ป๊ฒ ํด๊ฒฐํ๋์ง ์์ค๊น์?
๋ ๊ตฌ์ฒด์ ์ผ๋ก ๋ง์๋๋ฆฌ๋ฉด, VideoList ์ปดํฌ๋ํธ ๋ด์์ VideoItem์ปดํฌ๋ํธ ๋ชฉ๋ก์ ๋ ๋๋ง ํด์ฃผ๋๋ฐ, ์ด VideoList๊ฐ ๋ฌดํ์คํฌ๋กค์ด ์ ์ฉ๋ ์ํฉ์ ๋๋ค. ์ฒ์์๋ ๋ ๋๋ง์ด ์ผ์ด๋ ์ ๋ฐ์ ์๋๋ฐ, ์ดํ์ ๋ค์ ํ์ด์ง์ ํด๋นํ๋ ํ ํฐ์ ์๋ฒ๋ก๋ถํฐ ๋ฐ์์ ๋ค์ api ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ ๋ค์์ ํ๋ฉด์ ๋ฟ๋ ค์ค ๋ ์ ์ ์ฒด ๋ ๋๋ง์ด ํ ๋ฒ ๋ ์ผ์ด๋๋์ง... ํน์ ์์ ๋ค๋ฉด ๋ต์ฅ ๋ถํ๋๋ฆฝ๋๋ค.
์ข์ ๊ธ ๊ฐ์ฌํฉ๋๋ค! ์ฝ๋ ๋ธ๋ก ์์ ๋ถ๋ถ
+``` ๋ค์ javascript ๋ typescript ์ ๋ ฅํ์๋ฉด ์ฝ๋ ๊ฐ๋ ์ฑ์ด ์ข์์ง ๊ฒ ๊ฐ์์!