12.05 TIL - react-query

์•„๋ฆ„ยท2023๋…„ 12์›” 5์ผ
1

react-query

์‚ฌ์ „ ์ค€๋น„ ์‚ฌํ•ญ

json-server๋กœ ์ž„์‹œ ๋ฐฑ์—”๋“œ & DB ๋งŒ๋“ค๊ธฐ

// 1. json-server ์„ค์น˜
yarn add json-server

// 2. package.json์— ๋ช…๋ น์–ด ์ž…๋ ฅ
"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
		// ์—ฌ๊ธฐ์— ์ด๋ ‡๊ฒŒ ์ถ”๊ฐ€
    "run-server": "json-server --watch db.json --port 3001"
  },

// 3. json-server ์‹คํ–‰
yarn run run-server 

axios ์„ค์น˜ํ•˜๊ธฐ

yarn add axios

React-query๋ฅผ ์“ฐ๋Š” ์ด์œ 

// ๊ธฐ์กด์˜ ๋ฐฉ๋ฒ•

// 1. ๋ฐฑ์—”๋“œ๋กœ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ์š”์ฒญ ํ›„
await fetch("์ฃผ์†Œ", { method: "POST", ~~ })
// 2. redux ์—…๋ฐ์ดํŠธ ํ•ด์•ผ ํ•จ
dispatch(์ถ”๊ฐ€ํ•˜๊ธฐ(์ƒˆ๋กœ์šด์ƒํ’ˆ))

react-query๋Š” ๋ฐฑ์—”๋“œ ๋ฐ์ดํ„ฐ์™€ ํ”„๋ก ํŠธ์—”๋“œ ๋ฐ์ดํ„ฐ์˜ ์‹ฑํฌ๋ฅผ ๊ณ„์† ๋งž์ถฐ์คŒ -> ๊ด€๋ฆฌ๊ฐ€ ํŽธํ•จ

์ด์™ธ์—๋„ ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์„ ๋งŽ์ด ์ œ๊ณตํ•ด์คŒ

  • isLoading, isError ๋“ฑ์„ ์ง์ ‘ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์—†์Œ
  • ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ์‹  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ด์คŒ (์ฝ”์ธ๊ฑฐ๋ž˜์†Œ, SNS ๋“ฑ)
  • ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋‹ค์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญ
  • ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ค‘๋ณตํ•ด์„œ ์‚ฌ์šฉํ•ด๋„ ํšจ์œจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์คŒ (์บ์‹ฑํ•ด์คŒ)

React-query ์‚ฌ์šฉ์„ ์œ„ํ•œ ์„ธํŒ…

1. ์„ค์น˜ํ•˜๊ธฐ

yarn add react-query

2. react-query๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€ ๊ณณ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ธํŒ…ํ•˜๊ธฐ (index.js)

// react-query์—์„œ 2๊ฐœ import ํ•˜๊ธฐ
import { QueryClient, QueryClientProvider } from "react-query";
// ๋ณ€์ˆ˜ ์„ ์–ธํ•˜๊ธฐ
const queryClient = new QueryClient();
// QueryClientProvider ๋กœ ๊ฐ์‹ธ๊ธฐ
root.render(
  <QueryClientProvider client={queryClient}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </QueryClientProvider>
);

React-query๋กœ ๋ฐฑ์—”๋“œ๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ - useQuery

1. ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ useQuery import ํ•˜๊ธฐ

import { useQuery } from "react-query"
import axios from "axios";

2. useQuery ์‚ฌ์šฉํ•˜๊ธฐ (App.js)

const result = useQuery(๋ฌธ์ž์—ด๋กœ ์ด๋ฆ„์ง“๊ธฐ, ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜);

//
const result = useQuery("posts", async () => {
    const response = await axios.get("http://localhost:3001/posts");
    return response.data;
  });

3. result์— ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด๊ธฐ

console.log(result)

result๋Š” ๊ฐ์ฒด
๊ทธ ์ค‘ data๋งŒ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ {data} ๋ฝ‘์•„๋‚ด๊ธฐ

4. data๋ฅผ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜๊ธฐ

// data๋Š” ๊ฐ์ฒด๋ฅผ ์š”์†Œ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ฐฐ์—ด
return (
    <div>
      {data.map((post) => {
        return <div>{post.title}</div>;
      })}
    </div>
  );

error๊ฐ€ ๋‚˜๋Š” ์ด์œ  -> data๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆผ!

5. isLoading ํ™œ์šฉํ•˜๊ธฐ

  • isLoading์€ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ true, ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค ๊ฐ€์ ธ์˜จ ํ›„์—๋Š” false๋กœ ๋ฐ”๋€œ
const { data, isLoading } = useQuery("posts", async () => {
    const response = await axios.get("http://localhost:3001/posts");
    return response.data;
  });

if (isLoading) {
    return <div>๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘์ž„</div>;
  }

6. ์—๋Ÿฌ ์ฒ˜๋ฆฌํ•˜๊ธฐ

  • ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ์Œ
  • useQuery์—๋Š” isError์™€ error๋ผ๋Š” ๋ฐ์ดํ„ฐ๋„ ์กด์žฌํ•จ
 const { data, isLoading, isError, error } = useQuery("posts", async () => {
    const response = await axios.get("http://localhost:3001/posts");
    return response.data;
  });

if (isError) {
	return <div>{error.message}</div>
}

๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ํ•˜๊ธฐ - useMutation

  • useQuery๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์‚ฌ์šฉ
  • ๊ทธ์— ๋ฐ˜ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” useMutation ์‚ฌ์šฉ

1. useMutaion import ํ•˜๊ธฐ

import { useMutation } from "react-query";

2. useMutation ์‚ฌ์šฉํ•˜๊ธฐ

  • useMutation์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•จ์ˆ˜๋ฅผ ๋„ฃ๊ธฐ
  • useMutation์˜ return ๊ฐ’์„ ๋ณ€์ˆ˜ mutation์— ๋‹ด๊ธฐ
const mutation = useMutation(๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ or ์ˆ˜์ • or ์‚ญ์ œ ๊ด€๋ จ ํ•จ์ˆ˜);

const mutation = useMutation(async () => {
    await axios.post("http://localhost:3001/posts", {
      id: nanoid(),
      title: "ํ•˜ํ•˜",
      author: "๋ณธ์ธ",
    });
  });

3. ์ถ”๊ฐ€ํ•˜๊ธฐ ๋ฒ„ํŠผ ๋งŒ๋“ค๊ธฐ

  • useMutation์˜ ๋ฐ˜ํ™˜๊ฐ’์—์„œ .mutate()๋ฅผ ๋ถ™์—ฌ์คŒ
<button
    onClick={() => {
      mutation.mutate();
    }}
  >
    ๋ฐ์ดํ„ฐ์ถ”๊ฐ€!!
  </button>

๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ db.json์— ์ถ”๊ฐ€๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ
but, ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ํ›„ ํ™”๋ฉด์€ ๋ฐ”๋กœ ๋ฐ”๋€Œ์ง€ ์•Š๊ณ  ์žˆ์Œ

๋ฐ์ดํ„ฐ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ ํ›„ ํ™”๋ฉด ๋ฐ”๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ - useQueryClient

  • ๋ฐฑ์—”๋“œ์—๊ฒŒ โ€œ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๋ฐ›์•„๋ผ!โ€ ๋ผ๊ณ  ๋งํ•˜๋Š” ๊ฒƒ์ด์ง€ ํ™”๋ฉด์„ ๊ทธ๋ ค์ฃผ๋Š” ๊ฒƒ์€ ์•„๋‹˜
  • ๊ทธ๋ž˜์„œ react-query์—๊ฒŒ ๋‹ค์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Œ
  • mutation ์‹คํ–‰ ์„ฑ๊ณต ํ›„ ๊ธฐ์กด์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™”๋˜ useQuery๋ฅผ โ€œ๋ฌดํšจํ™”โ€์‹œ์ผœ ๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋งŒ๋“ค์–ด๋ณด์ž

1. useQueryClient๋ฅผ import ํ•˜๊ธฐ (App.js)

import { useQuery, useMutation, useQueryClient } from "react-query";

2. useQueryClient๋ฅผ ์‚ฌ์šฉํ•จ

const queryClient = new useQueryClient();

3. useMutation์˜ ๋‘๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ฝ”๋“œ ์ถ”๊ฐ€

  • posts๋Š” useQuery์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™”๋˜ ์ด๋ฆ„๊ณผ ๋˜‘๊ฐ™์•„์•ผ ํ•จ!
{
  onSuccess: () => {
    queryClient.invalidateQueries("posts");
  },
}

๊ณ ์ •๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์œ ๋™์ ์œผ๋กœ ๋„ฃ๊ธฐ

์œ„ ์ฝ”๋“œ๋Š” ๋‚ด๊ฐ€ ๋„ฃ์€ ๋ฐ์ดํ„ฐ๋งŒ ๋„ฃ์„ ์ˆ˜ ์žˆ์Œ

1. ๋งค๋ฒˆ ๋‹ค๋ฅธ ๊ฐ’์„ ๋„ฃ๊ธฐ ์œ„ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•ด๋ณด๊ธฐ

const mutation = useMutation(
	// ํ•จ์ˆ˜์— parameter ์ถ”๊ฐ€ ๋ฐ ์‚ฌ์šฉํ•˜๊ธฐ
  async (์ƒˆ๋กœ์šด๋ฐ์ดํ„ฐ) => {
    await axios.post("http://localhost:3001/posts", ์ƒˆ๋กœ์šด๋ฐ์ดํ„ฐ);
  },
  {
    onSuccess: () => {
      queryClient.invalidateQueries("posts");
    },
  }
);

2. mutation() ๋ถ€๋ถ„์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•˜๊ธฐ

<button
  onClick={() => {
    mutation.mutate({
      id: nanoid(),
      title: "useState๋กœ ์ž…๋ ฅ๋œ title",
      author: "useState๋กœ ์ž…๋ ฅ๋œ author",
    });
  }}
>
  ๋ฐ์ดํ„ฐ์ถ”๊ฐ€!!
</button>

์•„์›ƒ์†Œ์‹ฑ ํ”„๋กœ์ ํŠธ

ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

  • MotiTube (Motivation + YouTube)
    Motivation๊ณผ YouTube์˜ ๊ฒฐํ•ฉ์œผ๋กœ, ๋™๊ธฐ๋ถ€์—ฌ ๋™์˜์ƒ์„ ์ค‘์‹ฌ์œผ๋กœ ํ•œ ์„œ๋น„์Šค์ž„์„ ๊ฐ•์กฐ
  • ํ•œ ์ค„ ์ •๋ฆฌ : ์œ ํŠœ๋ธŒ API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™๊ธฐ๋ถ€์—ฌ ๋™์˜์ƒ์„ ์ถ”์ฒœํ•ด์ฃผ๋Š” ์›นํŽ˜์ด์ง€
  • ๋‚ด์šฉ : ์‚ฌ์šฉ์ž๋“ค์ด ๋™์˜์ƒ์— ๋Œ€ํ•œ ๋Œ“๊ธ€์„ ๋‚จ๊ธฐ๊ณ , ์ข‹์•„์š”๋ฅผ ํ‘œ์‹œ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ž์‹ ์ด ์ข‹์•„ํ•˜๋Š” ๋™์˜์ƒ์„ ์—…๋กœ๋“œํ•˜์—ฌ ์ถ”์ฒœ๋„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ณต๊ฐ„

๊ตฌํ˜„ ๊ธฐ๋Šฅ

  • ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ
    - ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” ํ…์ŠคํŠธ ํ•„๋“œ์™€ ์ „์†ก ๋ฒ„ํŠผ
    - ์†Œ์…œ ๋กœ๊ทธ์ธ
  • ๋งˆ์ด ํŽ˜์ด์ง€
    - ํ”„๋กœํ•„ ์‚ฌ์ง„๊ณผ ์‚ฌ์šฉ์ž ์ •๋ณด ํ‘œ์‹œ
    - ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋“ฑ๋ก, ๋ณ€๊ฒฝ, ๋‹‰๋„ค์ž„, ์†Œ๊ฐœ๊ธ€ ์ˆ˜์ •
    - ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก, ์ข‹์•„์š”ํ•œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก
  • ๋ฉ”์ธ ํŽ˜์ด์ง€
    - ๋„ค๋น„๊ฒŒ์ด์…˜ ํ—ค๋” (๋กœ๊ณ , ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ, ์œ ์ € ์•„์ด์ฝ˜, ํŽ˜์ด์ง€ ์ด๋™ ๋„ค๋น„๊ฒŒ์ด์…˜)
    - ์กฐํšŒ์ˆ˜ ๋†’์€ ์˜์ƒ ๋ชฉ๋ก์ˆœ์œผ๋กœ ๋ฐฐ์น˜
    - ์นด๋“œ์—๋Š” ๋™์˜์ƒ ์ œ๋ชฉ, ์—…๋กœ๋“œํ•œ ์‚ฌ์šฉ์ž ์ด๋ฆ„, ์กฐํšŒ์ˆ˜, ์ธ๋„ค์ผ, ์ข‹์•„์š” ์ˆ˜ ๋“ฑ์˜ ์ •๋ณด ํ‘œ์‹œ
    - ์นด๋“œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ๋™์˜์ƒ์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™
  • ์ƒ์„ธ ํŽ˜์ด์ง€
    - ๋™์˜์ƒ ํ”Œ๋ ˆ์ด์–ด์™€ ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ, ๋‚ด์šฉ, ๊ฒŒ์‹œ ์‹œ๊ฐ„, ๊ฐ„๋žตํ•œ ์ž‘์„ฑ์ž ์ •๋ณด
    - ์ข‹์•„์š”
    - ๋Œ“๊ธ€ ์ž‘์„ฑ ๋ฐ ๋Œ“๊ธ€ ๋ชฉ๋ก
  • ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํŽ˜์ด์ง€
    - ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ, ๋‚ด์šฉ, ๋™์˜์ƒ ๋งํฌ ์ž…๋ ฅํผ
  • ๊ฒ€์ƒ‰
    - ๊ฒŒ์‹œ๊ธ€, ์‚ฌ์šฉ์ž๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๊ฒฐ๊ณผ ํ‘œ์‹œ

์—ญํ•  ๋ถ„๋‹ด

๋‚ด๊ฐ€ ๋งก์€ ๋ถ€๋ถ„ : ํŒŒ์ด์–ด๋ฒ ์ด์Šค๋ฅผ ํ™œ์šฉํ•œ ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„

  • ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” ํ…์ŠคํŠธ ํ•„๋“œ์™€ ์ „์†ก ๋ฒ„ํŠผ
  • ์ธํ’‹์ฐฝ์— ์ž…๋ ฅ ์‹œ ํ•˜๋‹จ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋„์šฐ๊ธฐ - alert์ฐฝ X
  • ์†Œ์…œ ๋กœ๊ทธ์ธ (๊ตฌ๊ธ€)

๊ฐœ๋ฐœ ํ™˜๊ฒฝ

  • IDE: Visual Studio Code
  • OS: windows, Mac
  • Package Manager: Yarn Classic (v1.22.19)
  • React boilerplate: create-react-app

์‚ฌ์šฉ ๊ธฐ์ˆ  ์Šคํƒ

  • React - ์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋Š” UI๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„
  • Redux Toolkit - ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ ๋„๊ตฌ
  • React-router-dom - ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ผ์šฐํŒ…. URL์— ๋งž๋Š” ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
  • Styled-components - ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์Šคํƒ€์ผ ๊ด€๋ฆฌ. ์žฌ์‚ฌ์šฉ์ด ์‰ฌ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ๋™์  ์Šคํƒ€์ผ๋ง ์šฉ์ด
  • Firebase - ์‚ฌ์šฉ์ž ์ธ์ฆ๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ์˜ ์„œ๋ฒ„ ๊ธฐ๋Šฅ ์ œ๊ณต
  • React Query - ๋น„๋™๊ธฐ ๊ด€๋ จ ๋กœ์ง๊ณผ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ

์™€์ด์–ด ํ”„๋ ˆ์ž„


๋˜๋‹ค์‹œ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘์ด๋‹ค. ์ด๋ฒˆ์—๋Š” ํ•œ๋ฒˆ๋„ ์ง„ํ–‰ํ•ด๋ณด์ง€ ์•Š์•˜๋˜ firebase๋ฅผ ๋งก๊ฒŒ ๋ผ์„œ ๋„ˆ๋ฌด๋„ˆ๋ฌด ๊ฑฑ์ •๋˜์ง€๋งŒ ์–ด๋–ป๊ฒŒ๋“  ๋˜ ํ•ด๋‚ด์•ผ์ง€ ์–ด์ฉŒ๊ฒ ๋‚˜,,! ํž˜๋‚ด๋ณด์ž๐Ÿซ 

profile
๋‚ด ๊ฟˆ์€ ๊ฐœ๋ฐœ์ž

0๊ฐœ์˜ ๋Œ“๊ธ€