Skip to content

๐Ÿ”ฅ ํ•ต์‹ฌ ๊ธฐ์ˆ 

Jay edited this page Aug 8, 2023 · 7 revisions

1๏ธโƒฃ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„์ž…

ํŒ€์›๋“ค ๋ชจ๋‘ React ํ”„๋กœ์ ํŠธ๊ฐ€ ์ฒ˜์Œ์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— React์—์„œ ๋งŒ๋“ค์–ด์„œ ๋ฌธ๋ฒ• ์นœํ™”์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Recoil ์„ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •.

  • ํŒ€์›๋“ค๊ณผ ํ•จ๊ป˜ JD๋ฅผ ๋ถ„์„ํ•ด๋ณธ ๊ฒฐ๊ณผ ํ˜„์žฌ ํ”„๋ก ํŠธ์—”๋“œ ์—…๊ณ„์—์„œ ์š”๊ตฌํ•˜๋Š” ์—ญ๋Ÿ‰ ์ค‘ Fetch ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” axios ์™€ React Query ๋ฅผ ์šฐ๋Œ€ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธ.
  • ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•˜๋‚˜์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ๋งŒ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๊ฐ๊ฐ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์žฅ์ ์„ ์‚ด๋ ค Client State๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” Recoil๊ณผ ServerState๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ReactQuery๋ฅผ ์กฐํ•ฉํ•˜๋Š” ์ „๋žต์„ ์‹œํ–‰.

Client State

Recoil

  • Client๊ฐ€ ์†Œ์œ ํ•˜๋ฉฐ ์˜จ์ „ํžˆ ์ œ์–ด
  • ํ•ญ์ƒ Client ๋‚ด๋ถ€์— ์ €์žฅ๋˜๋ฉฐ ์กฐ์ž‘์— ๋”ฐ๋ผ ์ตœ์‹ ์ƒํƒœ๋กœ ๊ฐฑ์‹ 
  • Recoil์€ React์—์„œ ๋งŒ๋“  ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • key๋กœ ๊ตฌ๋ถ„๋˜๋Š” ๊ฐ atom์€, ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ atom์„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ๊ทธ ์ปดํฌ๋„ŒํŠธ๋“ค๋„ ๋™์ผํ•œ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๋ฏ€๋กœ atom์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด, ํ•ด๋‹น atom์„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋“ค์˜ย state๊ฐ€ ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง.
  • ํšŒ์›์˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” userDataAtom ํšŒ์›์˜ ๋กœ๊ทธ์ธ ์œ ๋ฌด์™€ API ํ†ต์‹ ์„ ์œ„ํ•œ Token์„ ์ €์žฅํ•˜๋Š” privateDataAtom ์„ ์ „์—ญ์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ €์žฅ.
  • ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ Modal ๋‰ด์Šค๋ ˆํ„ฐ๋ฅผ ์œ„ํ•œ EarthUs ๊ณต์‹ ๊ณ„์ • ์ •๋ณด ์— ๋Œ€ํ•œ ์ƒํƒœ๋ฅผ ์ถ”๊ฐ€๋กœ ์ „์—ญ์— ์ €์žฅ.

Server State

React Query

  • Fetching Updating ๋น„๋™๊ธฐ API ์‹คํ–‰
  • Client๊ฐ€ ์ œ์–ดํ•˜๊ฑฐ๋‚˜ ์†Œ์œ ํ•˜์ง€ ์•Š๋Š” ์›๊ฒฉ์˜ Server ๊ณต๊ฐ„์—์„œ ๊ด€๋ฆฌ
  • React Query ๋Š” ๋ฐ˜๋ณต์ ์ธ ๋น„๋™๊ธฐ API ํ†ต์‹ ์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์—…๋ฐ์ดํŠธ, ์บ์‹ฑ, ์—๋Ÿฌ์ฒ˜๋ฆฌ ๋“ฑ์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ.
  • DevTool์„ ์ œ๊ณตํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— API ํ†ต์‹ ์— ๋Œ€ํ•œ ๋””๋ฒ„๊น…์— ์šฉ์ด.
  • ๋น„๋™๊ธฐ๋กœ ์„œ๋ฒ„ ํ†ต์‹ ์— ์„ฑ๊ณตํ•˜๋ฉด data ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด error ๊ฐ์ฒด๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์œ ํ‹ธ ๊ธฐ๋Šฅ์„ ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •.

2๏ธโƒฃ API ํ˜ธ์ถœ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… ์ œ์ž‘

  • API ์š”์ฒญ๊ณผ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋กœ์ง์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก axios, ReactQuery ์‚ฌ์šฉ
  • ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค ํ†ต์‹ ์„ ํ•  ๋•Œ API ๋ช…์„ธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ์ปค์Šคํ…€ ํ›…์„ 4๊ฐ€์ง€๋กœ ๋ถ„๋ฅ˜ํ•ด์„œ ์ œ์ž‘.
  • useApiQuery
    • ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” HTTP METHODย GETย ์š”์ฒญ๊ณผ ๊ฐ™์ด ์„œ๋ฒ„์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ์ƒํƒœ๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์‚ฌ์šฉํ•  ๋•Œ ์‚ฌ์šฉ.
    • ์ปค์Šคํ…€ํ›… ์ž‘๋™ ๋ฐฉ์‹
ํŒŒ๋ผ๋ฏธํ„ฐ ์—ญํ• 
queryKey ์ „์†ก Url์œผ๋กœ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋„๋ก ์„ค๊ณ„.
queryFn Query ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•œ Promise๋ฅผ Return ํ•˜๋Š” ํ•จ์ˆ˜.
options useQuery์—์„œ ์‚ฌ์šฉ๋˜๋Š” refetchOnWindowFocus, enabled Option ๊ณตํ†ต ์ ์šฉ.
// useQuery ๊ตฌ์กฐ
const { isLoading, error, data } = useQuery(
    [apiUrl],         // queryKey
    executeQuery,     // queryFn
    {                 // Options
      // ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด์„ ์ดํƒˆํ–ˆ๋‹ค๊ฐ€ ๋‹ค์‹œ ํฌ์ปค์Šคํ•  ๋•Œ refetch ๋ฐฉ์ง€
      refetchOnWindowFocus: false,
      // ์ฟผ๋ฆฌ ์‹คํ–‰ ์—ฌ๋ถ€๋ฅผ Boolean์„ ํ†ตํ•ด ์ œ์–ด
      enabled,
    },
);
    • ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” HTTP METHODย GETย ์š”์ฒญ๊ณผ ๊ฐ™์ด ์„œ๋ฒ„์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ์ƒํƒœ๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์‚ฌ์šฉํ•  ๋•Œ ์‚ฌ์šฉ.
    • ์ปค์Šคํ…€ํ›… ์ž‘๋™ ๋ฐฉ
    • ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ useQuery์˜ queryFn ์˜์—ญ์— excuteQuery ํ•จ์ˆ˜๋ฅผ ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌ.
    • ์ „์†ก Url, ๋ฉ”์„œ๋“œ, body(json), enabled ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์€ ํ›„ axios๋ฅผ ํ™œ์šฉํ•ด์„œ API์— ์š”์ฒญ ์ „์†ก.
    • API ๋ช…์„ธ์— ๊ณตํ†ต์œผ๋กœ ๋“ค์–ด๊ฐ€๋Š” headers๋Š” ๊ณ ์ •์œผ๋กœ ์„ค์ •.
    • ์‘๋‹ต์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ isLoading error data ๋กœ ๋ฐ˜ํ™˜.
// ์ „์ฒด ์ฝ”๋“œ ๋กœ์ง
const executeQuery = async () => {
  try {
    const headers = {
      "Content-type": "application/json",
      Authorization: `Bearer ${token}`,
    };
    const res = await axios({
      url: BASE_URL + apiUrl,
      method: "",
      headers,
      data: body,
      enabled: true,
    });
    if (res.status === 200) {
      return res.data;
    }
  } catch (error) {
    return error;
  }
};

const { isLoading, error, data } = useQuery(...);
return { isLoading, error, data };
  • useApiMutation
    • ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ์–ดํ•˜๋Š” HTTP METHOD POST, PUT, DELETE ์š”์ฒญ๊ณผ ๊ฐ™์ด ์„œ๋ฒ„์— Side Effect๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ์ƒํƒœ์— ๋ณ€์ด๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ๋•Œ ์‚ฌ์šฉ.
    • ์ปค์Šคํ…€ ํ›… ์ž‘๋™ ๋ฐฉ์‹
ํŒŒ๋ผ๋ฏธํ„ฐ ์—ญํ• 
mutationKey ์ „์†ก Url์œผ๋กœ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋„๋ก ์„ค๊ณ„.
mutationFn Mutation ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•œ Promise๋ฅผ Return ํ•˜๋Š” ํ•จ์ˆ˜.
options useMutation์—์„œ ์‚ฌ์šฉ๋˜๋Š” Option ๊ฐ์ฒด๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํŽ˜์ด์ง€์—์„œ ์ง์ ‘ ์ž‘์„ฑํ•ด์„œ ์‚ฌ์šฉ.
// useMutation ๊ตฌ์กฐ
const mutations = useMutation(
  executeMutation, {    // mutationFn
  mutationKey: apiUrl,  // mutationKey
  ...options,           // options
});
    • ๋ฆฌ์•กํŠธ์ฟผ๋ฆฌ useMutation์˜ mutationFn ์˜์—ญ์— executeMutation ํ•จ์ˆ˜๋ฅผ ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌ.
    • ์ „์†ก Url, ๋ฉ”์„œ๋“œ, data, options ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์€ ํ›„ axios๋ฅผ ํ™œ์šฉํ•ด์„œ API์— ์š”์ฒญ ์ „์†ก.
    • API ๋ช…์„ธ์— ๊ณตํ†ต์œผ๋กœ ๋“ค์–ด๊ฐ€๋Š” headers๋Š” Token์ด ์žˆ๋Š”์ง€ ์‹๋ณ„ํ•œ ํ›„ ๊ณ ์ •์œผ๋กœ ์„ค์ •.
    • ์™ธ๋ถ€ ํŒŒ์ผ์—์„œ ์ปค์Šคํ…€ ํ›… ํ˜ธ์ถœ์‹œ ์ž…๋ ฅํผ์„ ์ž‘์„ฑํ•œ ํ›„ ๋ฒ„ํŠผ์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์— mutate ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ SideEffect ๋ฐœ์ƒ.
// ์ „์ฒด ์ฝ”๋“œ ๋กœ์ง
export default function useApiMutation(
  apiUrl = "", method = "", data = null, options = {},
) {
  const executeMutation = async () => {
    const headers = {
      "Content-type": "application/json",
    };
    token && (headers.Authorization = `Bearer ${token}`);

    const res = await axios({
      url: BASE_URL + apiUrl,
      method,
      headers,
      data,
    });
    return res.data;
  };

  const mutations = useMutation(executeMutation, {...});
  return mutations;
}
// ๊ฒŒ์‹œ๋ฌผ ์—…๋กœ๋“œ ํ•ธ๋“ค๋Ÿฌ ์ด๋ฒคํŠธ ์˜ˆ์‹œ
const handleUploadPost = e => {
  e.preventDefault();
  uploadPostMutation.mutate();
};
  • useImageUploader
    • useApiMutatuion Hook ๊ณผ ๊ฐ™์ด ์„œ๋ฒ„์— Side Effect๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ์ƒํƒœ์— ๋ณ€์ด๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉฐ <input type=โ€fileโ€/> ์„ ํ†ตํ•ด image ํ™•์žฅ์ž๋ฅผ ์—…๋กœ๋“œํ•˜๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ.
    • API ๋ช…์„ธ์—์„œ ์ด๋ฏธ์ง€ ๋“ฑ๋ก์ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€์—์„œ๋Š” ์„œ๋ฒ„์— ์ด๋ฏธ์ง€๋ฅผ ์ „์†กํ•˜๋ฉด ์ˆซ์ž๋กœ ์ด๋ฃจ์–ด์ง„ filename ์„ ํฌํ•จํ•˜๋Š” ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ํ•ด๋‹น ๊ธฐ์ค€์— ๋งž์ถฐ ์กฐ๊ฑด์‹ ์‹คํ–‰.
    • ์ปค์Šคํ…€ ํ›… ์ž‘๋™ ๋ฐฉ์‹
ํŒŒ๋ผ๋ฏธํ„ฐ ์—ญํ• 
mutationKey ์ „์†ก Url์œผ๋กœ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋„๋ก ์„ค๊ณ„.
mutationFn Mutation ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•œ Promise๋ฅผ Return ํ•˜๋Š” ํ•จ์ˆ˜.
options useMutation์—์„œ ์‚ฌ์šฉ๋˜๋Š” Option ๊ฐ์ฒด๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํŽ˜์ด์ง€์—์„œ ์ง์ ‘ ์ž‘์„ฑํ•ด์„œ ์‚ฌ์šฉ.
// ๋กœ์ปฌ ์ด๋ฏธ์ง€ ์„œ๋ฒ„์— ๋“ฑ๋ก
const executeImageUpload = async files => {
  const formData = new FormData();

  // files๊ฐ€ ๋ฐฐ์—ด์ธ์ง€ ์•„๋‹Œ์ง€ ํ™•์ธ ํ›„ ๋ฐฐ์—ด์ด ์•„๋‹ˆ๋ฉด ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜
  const filesArray = Array.isArray(files) ? files : [files];
  
  // ๋ฐฐ์—ด์— ์žˆ๋Š” ๋ชจ๋“  ํŒŒ์ผ์„ formData์— 'image'๋ผ๋Š” ํ‚ค๋กœ ์ถ”๊ฐ€
  filesArray.forEach(file => {
    formData.append("image", file);
  });

  // axios๋กœ formData๋ฅผ ์ „์†กํ•ด์„œ data ๋ฐ˜ํ™˜
  const response = await axios.post(BASE_URL + apiUrl, formData);
  return response.data;
};
// useMutation ๊ตฌ์กฐ
const mutation = useMutation(executeImageUpload, {
  onSuccess: data => {
    // ๋ฐ˜ํ™˜ ๋ฐ›์€ data๊ฐ€ ๋ฐฐ์—ด์ธ์ง€ ์‹๋ณ„ ํ›„ filename ์ถ”์ถœ
    let filenames;
    (Array.isArray(data)) {
      filenames = data.map(d => d.filename);
    } else {
      filenames = [data.filename];
    }

    // ์ด๋ฏธ์ง€ ์ฃผ์†Œ๋ฅผ BASE_URL๊ณผ ์กฐํ•ฉํ•ด์„œ image ์ƒํƒœ์— ์ €์žฅ.
    const apiImg = filenames
      .map(filename => `${BASE_URL}/${filename}`)
      .join(",");
    setImage(apiImg);
  },
// ์ „์ฒด ์ฝ”๋“œ ๋กœ์ง
export default function useImageUploader(apiUrl) {
  const [image, setImage] = useState("");

  const executeImageUpload = async files => {...};
  const mutation = useMutation(...);

  return { mutation, image };
}
    • ๋ฆฌ์•กํŠธ์ฟผ๋ฆฌ useMutation์˜ mutationFn ์˜์—ญ์— executeImageUpload ํ•จ์ˆ˜๋ฅผ ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌ.
    • ์ „์†ก Url, formData ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์€ ํ›„ axios๋ฅผ ํ™œ์šฉํ•ด์„œ API์— ์š”์ฒญ ์ „์†ก.
    • ํŒŒ์ผ์„ 2์žฅ ์ด์ƒ ๋“ฑ๋กํ–ˆ์„ ๊ฒฝ์šฐ filename ์ด ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜๋˜๋ฏ€๋กœ, ๋‹จ์ผ์ด๋ฏธ์ง€์™€ ๋™์ผํ•œ ๋กœ์ง์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด .join ๋ฐฐ์—ด๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋ฌธ์ž์—ด์„ ํ•˜๋‚˜๋กœ ์ทจํ•ฉํ•˜์—ฌ image ๋ณ€์ˆ˜์— ์ €์žฅ.
  • useApiInfiniteQuery

    • ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” HTTP METHODย GETย ์š”์ฒญ๊ณผ ๊ฐ™์ด ์„œ๋ฒ„์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ์ƒํƒœ๋ฅผ ๊ณ„์†ํ•ด์„œ ํ˜ธ์ถœํ•ด์„œ ์‚ฌ์šฉํ•  ๋•Œ ์‚ฌ์šฉ.
    • ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„์— ์‚ฌ์šฉ.
    • react-infinite-scroller ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ InfiniteScroll ์ปดํฌ๋„ŒํŠธ์™€ ๋†’์€ ํ˜ธํ™˜์„ฑ.
    • ์ปค์Šคํ…€ํ›… ์ž‘๋™ ๋ฐฉ์‹
ํŒŒ๋ผ๋ฏธํ„ฐ ์—ญํ• 
queryKey ์ „์†ก Url์œผ๋กœ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋„๋ก ์„ค๊ณ„.
queryFn InfiniteQuery ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•œ Promise๋ฅผ Return ํ•˜๋Š” ํ•จ์ˆ˜. pageParam ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ.
options useInfiniteQuery์—์„œ ์‚ฌ์šฉ๋˜๋Š” getNextPageParam Option ๊ณตํ†ต ์ ์šฉ.
// useInfiniteQuery ๊ตฌ์กฐ
const { data, hasNextPage, fetchNextPage, isLoading, isError } =
  useInfiniteQuery(
    [apiUrl],                 // queryKey
    executeInfiniteQuery,     // queryFn
    {                         // Options
      // ๋‹ค์Œ API ์š”์ฒญ์— ์‚ฌ์šฉ๋  pageParam ๋ฐ˜ํ™˜
      // ํ•œ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ ๊ธธ์ด๊ฐ€ LIMIT๋ณด๋‹ค ์ž‘์„ ๊ฒฝ์šฐ undefined ๋ฐ˜ํ™˜
      // ๊ทธ ์™ธ์˜ ๊ฒฝ์šฐ ํ˜„์žฌ ํŽ˜์ด์ง€ ์ˆ˜ * LIMIT ๋ฐ˜ํ™˜
      getNextPageParam: (lastPage, allPages) => {
        const resKey = keyName;
        if (
          keyName
            ? lastPage[resKey] && lastPage[resKey].length < LIMIT
            : lastPage && lastPage.length < LIMIT
        ) {
          return undefined;
        }
        return { skip: allPages.length * LIMIT };
      },
    }
);
    • ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ useInfiniteQuery์˜ queryFn ์˜์—ญ์— excuteInfiniteQuery ํ•จ์ˆ˜๋ฅผ ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌ.
    • excuteInfiniteQuery ํ•จ์ˆ˜์—์„œ pageParam ์„ ์ธ์ž๋กœ ์ „๋‹ฌํ•˜์—ฌ API ์š”์ฒญ ์ „์†ก Url์˜ skip ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ.
    • ์ „์†ก Url, ์‘๋‹ต key ๊ฐ’ ์„ ์ธ์ž๋กœ ๋ฐ›์€ ํ›„ axios get ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ API์— ์š”์ฒญ ์ „์†ก.
    • API ๋ช…์„ธ์— ๊ณตํ†ต์œผ๋กœ ๋“ค์–ด๊ฐ€๋Š” headers๋Š” ๊ณ ์ •์œผ๋กœ ์„ค์ •.
    • ์‘๋‹ต์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ data hasNextPage fetchNextPage isLoading isError ๋กœ ๋ฐ˜ํ™˜.
// ์ „์ฒด ์ฝ”๋“œ ๋กœ์ง
const executeInfiniteQuery = async ({ pageParam = { skip: 0 } }) => {
  const { skip } = pageParam;
  try {
    const headers = {
      "Content-type": "application/json",
      Authorization: `Bearer ${token}`,
    };
    const res = await axios.get(
      `${BASE_URL + apiUrl}?limit=${LIMIT}&skip=${skip}`,
      { headers },
    );
    if (res.status === 200) {
      console.log("์š”์ฒญ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.");
      console.table(res.data);
      return res.data;
    }
  } catch (error) {
    return error;
  }
};

const { data, hasNextPage, fetchNextPage, isLoading, isError } =
  useInfiniteQuery(...);
return { data, hasNextPage, fetchNextPage, isLoading, isError };
    • hasNextPage fetchNextPage ๋Š” InfiniteScroll ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ. hasMore ๊ฐ€ true์ธ ๊ฒฝ์šฐ loadMore ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋กœ๋“œ.
<InfiniteScroll hasMore={hasNextPage} loadMore={() => fetchNextPage()} />

3๏ธโƒฃ ์‹œ๋งจํ‹ฑ ๋งˆํฌ์—…

๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ•˜์ด๋ผ์ดํŠธ

  • ๊ฒ€์ƒ‰ API๋ฅผ ํ†ตํ•œ ๊ธฐ๋Šฅ ๊ตฌํ˜„์—์„œ ์ผ์น˜ํ•˜๋Š” ๊ฒ€์ƒ‰์–ด์— ํ•˜์ด๋ผ์ดํŠธ๋ฅผ ํ‘œ์‹œํ•  ๋•Œ ์‚ฌ์šฉ.
  • <mark>๋Š” ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ํ™œ๋™๊ณผ ์—ฐ๊ด€์ด ์žˆ๋Š” ๋ถ€๋ถ„์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ ์ฃผ์˜๋ฅผ ๋Œ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ํƒœ๊ทธ.
  • ๋”ฐ๋ผ์„œ, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ…์ŠคํŠธ์— ์‹œ๋งจํ‹ฑํ•œ ์˜๋ฏธ๋ฅผ ๋ถ€์—ฌํ•˜๊ธฐ ์œ„ํ•ด <mark>๋ฅผ ์ ์šฉํ•ด ์Šคํƒ€์ผ๋ง.
  // Link ๋žœ๋”๋ง ์กฐ๊ฑด๋ถ€ ์ถœ๋ ฅ
  function renderLinkContent() {
    // Search ํŽ˜์ด์ง€ ํ•˜์ด๋ผ์ดํŠธ ๊ด€๋ จ
    if (SEARCH) {
      const regex = new RegExp(searchKeyword, "gi");
      const userNameWithHighlight = userName.replace(
        regex,
        match => `<mark class="highlight">${match}</mark>`,
      );

      return (
        <>
          <Avatar profileImg={profileImg} size={40} />
          <div>
            <span dangerouslySetInnerHTML={{ __html: userNameWithHighlight }} />
            {id ? <p>@{account}</p> : ""}
          </div>
        </>
      );
    }

    return null;
  }

4๏ธโƒฃ ์›น ์ ‘๊ทผ์„ฑ

๋””์ž์ธ

  • ๊ธ€๊ผด ๋ฐ ๋ฐฐ๊ฒฝ ์ƒ‰ ๋ช…๋„ ๋Œ€๋น„ ์ค€์ˆ˜. ์›น์ ‘๊ทผ์„ฑ ์ค€์ˆ˜

  • ๋ฒ„ํŠผ์„ ์•„์ด์ฝ˜์œผ๋กœ๋งŒ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๋ฌธ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ง๊ด€์ ์œผ๋กœ ์˜๋ฏธ ์ „๋‹ฌ.

ํ‚ค๋ณด๋“œ

  • swiper slide ํƒญ์œผ๋กœ ํŽ˜์ด์ง€ ์ „ํ™˜ ๊ฐ€๋Šฅ.
  • ๋ชจ๋‹ฌ์ด ์—ด๋ ธ์„ ๋•Œ ๋ชจ๋‹ฌ ์•ˆ์—์„œ ํฌ์ปค์Šค ์žƒ์ง€ ์•Š๊ณ  ์ด๋™.
  • <input type=โ€œfileโ€ />์˜ ํŒŒ์ผ ์„ ํƒ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๋ฒ„ํŠผ ํƒญ ํ‚ค๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅ.
    <input type=โ€œfileโ€ />์˜ ํŒŒ์ผ ์„ ํƒ ๋ฒ„ํŠผ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š”๋ฐ label ํƒœ๊ทธ ์•ˆ์— button ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์–ด div ํƒœ๊ทธ์— ํƒญ ํ‚ค๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก role=โ€œbuttonโ€ tabIndex={0}์†์„ฑ ์ถ”๊ฐ€.

์Šคํฌ๋ฆฐ ๋ฆฌ๋”

  • ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์‹œ, ๊ฒ€์ƒ‰์–ด์™€ ์ผ์น˜ํ•˜๋Š” ํ•˜์ด๋ผ์ดํŠธ ๋ถ€๋ถ„์„ ์‹œ๊ฐ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ฒญ๊ฐ์œผ๋กœ๋„ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์Œ์„ฑ ๊ฐ•์กฐ ํšจ๊ณผ ์ถ”๊ฐ€.
  mark::before,
  mark::after {
  clip-path: inset(100%);
  clip: rect(1px, 1px, 1px, 1px);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

mark::before {
  content: " [๊ฐ•์กฐ ์‹œ์ž‘] ";
}

mark::after {
  content: " [๊ฐ•์กฐ ๋] ";
}
  • ์ฑ„ํŒ…๋ฐฉ์— ์ƒˆ๋กœ์šด ๋ฉ”์„ธ์ง€ ์ „์†ก ์‹œ, ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๋ฉ”์„ธ์ง€ ๋…ธ๋“œ๋ฅผ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ aria-live ๋ฐ role ์†์„ฑ ์ ์šฉ.
<section
  className="sendBubble"
  role="log"
  aria-live="polite"
  aria-label="Chat"
>
  {messages.map(message => (
    <ChatBubble
      key={message.key}
      isReceived={false}
      sentMessage={message.content}
      uploadedImage={message.uploadedImage}
      currentTime={message.time}
    />
  ))}
</section>

5๏ธโƒฃ Figma ๋””์ž์ธ์‹œ์Šคํ…œ ๋„์ž…

  • ํ”ผ๊ทธ๋งˆ Variable ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ ๋ฒ„ํŠผ hover focus ๋“ฑ ๋‹ค์–‘ํ•œ ํƒ€์ž…์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋””์ž์ธ ์‹œ์Šคํ…œ ์ œ์ž‘.
  • StyleComponent ๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ์ž‘ํ•˜๊ณ  props๋ฅผ ํ†ตํ•ด ํƒ€์ž…์„ ์ •์˜ํ•˜์—ฌ ์žฌ์‚ฌ์šฉ์„ฑ ์ฆ์ง„.

๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ ํ™œ์šฉ ์˜ˆ์‹œ

  <Button
    size="cta"
    variant={!disabledBtn && "primary"}
    type="submit"
  >
    ๋กœ๊ทธ์ธ
  </Button>

๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ ๋กœ์ง

const VARIANTS = {
  primary: css`
    --button-color: var(--color-white);
    --button-background: var(--color-primary);
    --button-border: 1px solid var(--color-primary);
    &::before {
      filter: invert(1);
    }
  `,
  white: css`
    --button-color: var(--color-black);
    --button-background: var(--color-white);
    --button-border: 1px solid var(--color-light);
    &::before {
      filter: invert(36%) sepia(93%) saturate(1033%) hue-rotate(183deg)
        brightness(88%) contrast(89%);
    }
  `,
};

const SIZES = {
  sm: css`
    --button-font-size: var(--font-size-xs);
    --button-padding: 0 1rem;
    --button-height: 2.25rem;
  `,
  md: css`
    --button-font-size: var(--font-size-md);
    --button-padding: 0 4rem;
    --button-height: 3rem;
  `,
  lg: css`
    --button-font-size: var(--font-size-lg);
    --button-padding: 0 6rem;
    --button-height: 4rem;
  `,
  cta: css`
    --button-font-size: var(--font-size-xl);
    --button-padding: 0 1rem;
    --button-height: 4rem;
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: 1rem;
    width: min(calc(100% - 2rem), calc(var(--size-max-width) - 2rem));
    border-radius: 0;
    align-items: center;
    justify-content: center;
    z-index: 10;
  `,
};

export default function Button({
  size,
  variant,
  children,
  icon,
  disabled,
  ...props
}) {
  return (
    // eslint-disable-next-line react/jsx-props-no-spreading
    <StyledButton
      icon={icon}
      size={size}
      variant={variant}
      disabled={disabled}
      {...props}
    >
      <span>{children}</span>
    </StyledButton>
  );
}

const StyledButton = styled.button`
  ${({ variant }) => VARIANTS[variant]}
  ${({ size }) => SIZES[size]}

  font:inherit;
  cursor: pointer;
  margin: 0;
  background: var(--button-background);
  border: var(--button-border);
  color: var(--button-color);
  padding: var(--button-padding);
  height: var(--button-height);
  line-height: var(--button-height);
  font-size: var(--button-font-size);
  border-radius: 0.25rem;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  transition: all 0.4s ease-out;
  &:disabled {
    --button-color: var(--color-gray-76);
    --button-background: var(--color-bg);
    --button-border: 1px solid var(--color-bg);
    cursor: not-allowed;
    pointer-events: none;
  }

  &:hover {
    filter: brightness(0.9);
  }

  // icon์ด ์žˆ๋Š” ๊ฒฝ์šฐ
  ${({ icon }) =>
    icon &&
    css`
      &::before {
        content: "";
        display: inline-block;
        width: 1.5rem;
        height: 1.5rem;
        background: url(${props => props.icon}) no-repeat center/1.5rem;
`}
`;

๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ