/RecordMyDay

๐Ÿ“† TODOApp with React, NextJS, TypeScript, React Query, Recoil

Primary LanguageTypeScript

RecordMyDay v2.0.0

๐ŸŒŸ์„œ๋น„์Šค์†Œ๊ฐœ

โœ๏ธ ๋‚˜์˜ ํ•˜๋ฃจ ์ผ์ •์— ๋Œ€ํ•ด ๊ณ„ํšํ•˜๊ณ  ์ˆ˜ํ–‰ ์‹œ๊ฐ„์„ ๊ธฐ๋กํ•˜๊ณ  ๊ณผ๊ฑฐ์˜ ๊ธฐ๋ก๋“ค์„ ๋ณผ ์ˆ˜ ์žˆ๋Š” ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ โœ๏ธ.

๐Ÿ›  Tech ๐Ÿ› 



โœจ V2์—์„œ๋Š” V1์—์„œ ๊ธฐ์ˆ  ์Šคํƒ ๋ณ€๊ฒฝ(JavaSciprt, Redux, ReduxSaga => TypeScript, Recoil, ReactQuery), UI ์—…๋ฐ์ดํŠธ, ๊ธฐ์กด ๋กœ์ง ์ˆ˜์ •์ด ์ด๋ฃจ์–ด์กŒ์Šต๋‹ˆ๋‹ค.

โœจ ๊ธฐ์ˆ  ์Šคํƒ์„ ๋ณ€๊ฒฝํ•œ ์ด์œ 

  • ๊ธฐ์กด Redux store๋ฅผ ์ด์šฉํ•ด ๊ตฌํ˜„ํ•œ V1์€ Redux๋ฅผ ์ด์šฉํ•ด, ์ „์—ญ ์ƒํƒœ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ๊ทธ๋Ÿฌ๋‚˜ ์‹ค์ œ store์—๋Š” ์ „์—ญ ์ƒํƒœ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ฝ”๋“œ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๋น„๋™๊ธฐ ํ†ต์‹ ์„ ์œ„ํ•œ ๋ถ€๋ถ„์ด ์ƒ๋‹นํžˆ ๋งŽ์€ ๋ถ€๋ถ„์„ ์ฐจ์ง€ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์ด์— ๋”ฐ๋ผ, ์ „์—ญ Store๋ผ๋Š” Redux๊ฐ€ ์ •๋ง Store๋ผ๋Š” ๋ณธ์งˆ์— ๋งž๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์ธ์ง€์— ๋Œ€ํ•œ ์ƒ๊ฐ์„ ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • ๊ทธ ๊ฒฐ๋ก ์œผ๋กœ, ๊ฐ€์žฅ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹์œผ๋กœ Store๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์€ Client์—์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” Client state๋งŒ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹์ด๋ผ๋Š” ๊ฒฐ๋ก ์„ ๋‚ด๋ ธ์Šต๋‹ˆ๋‹ค.

  • ๊ทธ ํ›„, Client์—์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” Client State์™€ Server์—์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” Server State๋ฅผ ๋‚˜๋ˆ ์„œ ์ƒ๊ฐํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ๊ฐ๊ฐ์— ๋‹ค๋ฅธ ๊ธฐ์ˆ ์„ ์ ์šฉํ•ด ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • ์ด์— ์„ ํƒ๋œ ๊ธฐ์ˆ ์ด Server State ๊ด€๋ฆฌ์—๋Š” React Query, Client State๊ด€๋ฆฌ์—๋Š” Recoil์ž…๋‹ˆ๋‹ค.

๐ŸŒŸ์ฃผ์š”๊ธฐ๋Šฅ

๐Ÿ˜Š ๋ฉ”์ธํ™”๋ฉด

  • ๋ฉ”์ธํ™”๋ฉด์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํ–ˆ์„ ๋•Œ์™€, ๋กœ๊ทธ์ธ ํ•˜์ง€ ์•Š์•˜์„ ๋•Œ๋ฅผ ๋‚˜๋ˆ„์–ด ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค
  • ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋Š”, React Query์˜ useQuery hook์„ ์ด์šฉํ•ด, ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ํ–ˆ๋Š”์ง€ ํ•˜์ง€ ์•Š์•˜๋Š”์ง€ ์ •๋ณด๋ฅผ ๋ฐ›์•„์™€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฉ”์ธํ™”๋ฉด ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๋ชจ๋“  ํ™”๋ฉด์—์„œ ๊ฐ™์€ hook์„ ์‚ฌ์šฉํ•ด, ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์œ ๋ฌด๋ฅผ ์ฒดํฌํ•ฉ๋‹ˆ๋‹ค.
  const { data: Userdata, isLoading: getUserInfoLoading } = useUserInfoQuery();

  useEffect(() => {
    if (!getUserInfoLoading) {
      if (!Userdata) {
        alert("*๋กœ๊ทธ์ธํ›„ ์ด์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค");
        router.push("/");
      }
    }
  }, [getUserInfoLoading]);
  
  export const useUserInfoQuery = () =>
  useQuery<UserInfo>("userInfo", () => getMyInfoAPI(), {
    refetchOnWindowFocus: false,
    staleTime: Infinity,
    refetchOnMount: false,
  });
  • useQuery์˜ ์˜ต์…˜์„ ์ฃผ์–ด, ํ•œ๋ฒˆ ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ํ™•์ธํ•œ ๋’ค, ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์— ๋‹ค์‹œ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ , ์ฒซ ์š”์ฒญ์˜ ๊ฐ’์„ ์บ์‹ฑํ•ด์„œ ์‚ฌ์šฉํ•˜๋„๋ก ๋งŒ๋“ค์–ด, ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์˜ ๋ถ€๋‹ด์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.

  • ๊ทธ๋ ‡์ง€๋งŒ, ๋กœ๊ทธ์ธ ํ›„, ๋กœ๊ทธ์•„์›ƒ ํ›„์—๋Š”, ์œ ์ €์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ฟผ๋ฆฌ์˜ ๊ฐ’์„ ์ƒˆ๋กญ๊ฒŒ ์ตœ์‹ ํ™” ์‹œ์ผœ์ฃผ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์ด๋Š” ์•„๋ž˜, ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ๋ถ€๋ถ„์— ์„ค๋ช…์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.

๐Ÿ‘‹ ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ

  • ํšŒ์›๊ฐ€์ž…์˜ ๊ฒฝ์šฐ์—๋Š” Database ์„œ๋ฒ„๋ฅผ ๋”ฐ๋กœ ๋‘์ง€ ์•Š์•˜์ง€๋งŒ, BackEnd Server์—์„œ MySQL Database์•ˆ์— ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ •๋ณด๋ฅผ ์ €์žฅํ•ด๋†“์Šต๋‹ˆ๋‹ค.
  • RecordMyDay ์ž์ฒด ์›น์‚ฌ์ดํŠธ์— ๊ฐ€์ž…์„ ํ•  ์ˆ˜๋„ ์žˆ์œผ๋ฉฐ, KaKao, Naver, Google OAuth2 ๋ฅผ ํ†ตํ•ด, ์†Œ์…œ ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ์„ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

OAuth2 ์„ค๊ณ„๋„ ์ด๋ฏธ์ง€ ์ถœ์ฒ˜ - http://blogs.innovationm.com/spring-security-with-oauth2/

  • OAuth๋ฅผ ํ†ตํ•œ ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ์„ ๊ทธ๋ฆผ์œผ๋กœ ๋‚˜ํƒ€๋‚ด๋ฉด ์œ„ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Resource Server (์šฐ๋ฆฌ ์„œ๋น„์Šค์—์„œ๋Š” KaKao, Facebook, Google) ์„ ํ†ตํ•ด, ๋ฏผ๊ฐํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌ ํ•˜์ง€ ์•Š๊ณ , Resource Server๋ฅผ ํ†ตํ•ด ์ œ๊ณต๋˜๋Š” ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ๋กœ์ปฌ ๋กœ๊ทธ์ธ๊ณผ ๋กœ๊ทธ์•„์›ƒ์€, React Query์˜ useMutation hook์„ ํ†ตํ•ด, ์„œ๋ฒ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค

  • ์œ„ ๋ฉ”์ธํŽ˜์ด์ง€์—์„œ ์–ธ๊ธ‰ํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” hook์€ ํ•œ๋ฒˆ caching๋˜๋ฉด, ๋‹ค์‹œ ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๋„๋ก ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

  • ๊ทธ๋ ‡์ง€๋งŒ, ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ์˜ ๊ฒฝ์šฐ์—๋Š” ํ•ด๋‹น hook์˜ retrun value๋ฅผ ๋ณ€๊ฒฝ์‹œ์ผœ์ฃผ๊ฑฐ๋‚˜, Refetch ํ•  ์ˆ˜ ์žˆ๋„๋ก, ํ•ด๋‹น ์ฟผ๋ฆฌ๋ฅผ Invalidate ์‹œํ‚ค๋Š” ๋ฐฉ์‹์„ ํ†ตํ•ด, caching ๋œ ๊ฐ’์„ ๋ณ€๊ฒฝ์‹œ์ผœ์ฃผ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์ด์— ๋”ฐ๋ผ, useMutation์˜ onSuccess callback์„ ํ†ตํ•ด, ๋กœ๊ทธ์ธ์˜ ๊ฒฝ์šฐ์—๋Š” hook์˜ return value ๋ณ€๊ฒฝ, ๋กœ๊ทธ์•„์›ƒ์˜ ๊ฒฝ์šฐ์—๋Š” ์ฟผ๋ฆฌ invalidation์„ ์‹œ์ผœ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  export const useLoginMutation = (onSuccess: (data: User) => void) => {
  const queryClient = useQueryClient();
  return useMutation(loginAPI, {
    onSuccess: (data) => {
      queryClient.setQueryData("userInfo", data);
      onSuccess(data);
    },
  });
};
  
  export const useLoginMutation = (onSuccess: (data: User) => void) => {
  const queryClient = useQueryClient();
  return useMutation(loginAPI, {
    onSuccess: (data) => {
      queryClient.setQueryData("userInfo", data);
      onSuccess(data);
    },
  });
};

๐Ÿ˜š ๊ณ„ํš์งœ๊ธฐ

  • ๊ณ„ํš์„ ์งœ๋Š” ์„œ๋น„์Šค๋Š” ์ด 3๋‹จ๊ณ„๋กœ ๋‚˜๋‰˜์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฐ ๋‹จ๊ณ„๋Š” ๋…๋ฆฝ๋œ component๋“ค๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ์Šต๋‹ˆ๋‹ค.
{activeStep === ๋‚ ์งœ_์„ค์ •ํ•˜๊ธฐ && <PickDate />}
{activeStep === ๊ณ„ํš_์„ค์ •ํ•˜๊ธฐ && <SettingPlan />}
{activeStep === ์„ค์ •_์™„๋ฃŒ && <Complete />}
  • ๋…๋ฆฝ๋œ Component๋“ค์—์„œ step(ํ˜„์žฌ ์–ด๋–ค ๋‹จ๊ณ„์ธ์ง€๋ฅผ ์ €์žฅ)์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ, props๋ฅผ ์ด์šฉํ•˜์ง€ ์•Š๊ณ , recoil์˜ atom์„ ํ†ตํ•ด, ์ „์—ญ ์Šคํ† ์–ด์—์„œ ๊ด€๋ฆฌํ•˜๊ณ , ํ•ด๋‹น ๊ฐ’์„ ์ „์—ญ ์Šคํ† ์–ด์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
export const ActiveStep = atom({
  key: "ActiveStep",
  default: 0,
});

const setActiveStep = useSetRecoilState(ActiveStep);

const AddScheduleSuccessFunction = useCallback(() => { //์„ฑ๊ณต์‹œ ๋‹ค์Œ step์œผ๋กœ,
    setActiveStep((prevStep) => prevStep + 1);
 }, []);

1โƒฃ ๋‚ ์งœ ์„ค์ •ํ•˜๊ธฐ

  • ์ฒซ ๋‹จ๊ณ„์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ๋‚ ์งœ๋ฅผ ์„ ํƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์›ํ•˜๋Š” ๋‚ ์งœ๋ฅผ ์„ ํƒํ•œ ํ›„, useMutation hook์„ ์ด์šฉํ•ด, ์„œ๋ฒ„์— ํ•ด๋‹น ๋‚ ์งœ๋ฅผ ๋ณด๋‚ด๊ณ  ์ €์žฅํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋•Œ onSuccess ์™€ onFailure์— ๋Œ€ํ•œ callback ํ•จ์ˆ˜๋ฅผ ๋งค๊ฒŒ๋ณ€์ˆ˜๋กœ ๋„ฃ์–ด, ์„ฑ๊ณต๊ณผ ์‹คํŒจ์— ๋Œ€ํ•œ ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  const AddScheduleSuccessFunction = useCallback(() => {
    setActiveStep((prevStep) => prevStep + 1);
  }, []);

  const AddScheduleFailureFunction = useCallback((data) => {
    if (data) {
      alert("*ํ•ด๋‹น ๋‚ ์งœ์— ์ด๋ฏธ ๊ณ„ํš์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค");
    }
    setSelectedDate(null);
  }, []);

  const addScheduleMutation = useAddScheduleMutation(AddScheduleSuccessFunction, AddScheduleFailureFunction);

2โƒฃ ๊ณ„ํš ์„ค์ •ํ•˜๊ธฐ

  • ๋‘๋ฒˆ์งธ ๋‹จ๊ณ„์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ๋‚ ์งœ์— ์›ํ•˜๋Š” ๊ณ„ํš์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • โž• ๋ฒ„ํŠผ๊ณผ โž– ๋ฒ„ํŠผ์„ ์ด์šฉํ•ด ์›ํ•˜๋Š” ๊ณ„ํš์„ ์ž…๋ ฅํ•˜๋Š” ๊ณต๊ฐ„์„ ๋™์ ์œผ๋กœ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ณ„ํš์„ ์ž…๋ ฅํ•œ ํ›„ "๋“ฑ๋ก" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ•ด๋‹น ๊ณ„ํš์ด BackEnd ์„œ๋ฒ„์— ์ €์žฅ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์ €์žฅ๋˜๋Š” ๊ณ„ํš์€ ์ฒ˜์Œ ์„ค์ •ํ•œ ๋‚ ์งœ์™€ hasMany ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๋Š” ํ…Œ์ด๋ธ”์— ์ €์žฅ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ๋ชจ๋“  ๊ณ„ํš์ด "๋“ฑ๋ก" ๋œ ์ดํ›„์—๋Š” "๋“ฑ๋ก์™„๋ฃŒ" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด, ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ์ด๋•Œ, ๊ฐ ๊ณ„ํš์€ ๋…๋ฆฝ๋œ component์ด๊ณ , ๋“ฑ๋ก์ด๋ผ๋Š” ๋ฒ„ํŠผ์€ ๋ถ€๋ชจ component์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
  • ์ด์ „ ๋ฒ„์ „์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋“  ๊ณ„ํš์„ ๋“ฑ๋กํ•˜์ง€ ์•Š๋”๋ผ๋„, ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐˆ ์ˆ˜ ์žˆ์ง€๋งŒ, V2์—์„œ๋Š” Recoil์˜ ์ „์—ญ ์Šคํ† ์–ด๋ฅผ ์ด์šฉํ•ด, ํ˜„์žฌ ์ œ์ถœํ•œ ๊ณ„ํš์˜ ๊ฐœ์ˆ˜์™€ โž• ๋ฒ„ํŠผ์„ ํ†ตํ•ด, ์ƒ์„ฑํ•œ ๊ณ„ํš ์ž…๋ ฅ ๊ณต๊ฐ„์˜ ๊ฐœ์ˆ˜๋ฅผ ๋น„๊ตํ•ด, ๋ชจ๋‘ ์ž…๋ ฅํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด, ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๊ฐ€์ง€ ๋ชปํ•˜๋„๋ก blockingํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  const completePlanFormNum = useRecoilValue(CompletePlanFormNum);
  const DayInfo = useRecoilValue(PickDateInfo);
  
  const submitPlanComplete = useCallback(() => {
  
    if (planFormNum !== completePlanFormNum) {
      alert("*์•„์ง ๋“ฑ๋กํ•˜์ง€ ์•Š์€ ๊ณ„ํš์ด ์žˆ์Šต๋‹ˆ๋‹ค");
      return;
    }
    
    setActiveStep((prevStep) => prevStep + 1);
  }, [planFormNum, completePlanFormNum]);

3โƒฃ ์„ค์ • ์™„๋ฃŒ

  • ๋ชจ๋“  ์„ค์ •์ด ์™„๋ฃŒ๋˜๋ฉด, ์„ค์ • ์™„๋ฃŒ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , ์‚ฌ์šฉ์ž๋ฅผ ๋ฉ”์ธํŽ˜์ด์ง€๋กœ ๋Œ๋ ค๋ณด๋ƒ…๋‹ˆ๋‹ค.

๐Ÿ˜ ์ผ์ •ํ™•์ธ ๋ฐ ์ˆ˜ํ–‰์‹œ๊ฐ„ ์ž…๋ ฅ

  • ์˜ค๋Š˜์ผ์ •์— ๋“ค์–ด๊ฐ€๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ์ด ์ „์— ์„ค์ •ํ•ด๋‘” ๋‹น์ผ์˜ ๊ณ„ํš๋“ค์ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ Component๋“ค์˜ ๋ฐฐ์—ด๋กœ ๋ณด์—ฌ์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž๋Š” ํ•ด๋‹น ์ผ์ •์„ ์‚ญ์ œํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์‹œ์ž‘ ์‹œ๊ฐ„๋งŒ ์„ค์ •ํ•ด ์ œ์ถœ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์‹œ์ž‘ ์‹œ๊ฐ„๊ณผ ๋งˆ๋ฌด๋ฆฌ ์‹œ๊ฐ„์˜ ์ œ์ถœ์€ useMutation hook์„ ์ด์šฉํ•ด ์ฒ˜๋ฆฌ๋˜๋Š”๋ฐ, useMutation hook์˜ onSuccess Callback์„ ํ†ตํ•ด, ์ฒ˜์Œ useQuery๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜จ ๊ณ„ํš์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝ์‹œ์ผœ์ค๋‹ˆ๋‹ค.
  • ์‚ญ์ œ ์—ญ์‹œ useMutation hook์„ ์ด์šฉํ•˜๋ฉฐ, onSuccess callback์„ ํ†ตํ•ด, ์ฒ˜์Œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝ์‹œ์ผœ์ค๋‹ˆ๋‹ค.
  • ์‹œ์ž‘ ์‹œ๊ฐ„๊ณผ ๋งˆ๋ฌด๋ฆฌ ์‹œ๊ฐ„์„ ์ œ์ถœํ•˜๊ฒŒ ๋˜๋ฉด, ์ด๋Š” BackEnd์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
  • BackEnd ์„œ๋ฒ„๋Š” ์‹œ์ž‘ ์‹œ๊ฐ„๊ณผ ๋งˆ๋ฌด๋ฆฌ ์‹œ๊ฐ„์˜ ์ฐจ์ด๋ฅผ ํ†ตํ•ด, ์ˆ˜ํ–‰ ์‹œ๊ฐ„์„ ๊ตฌํ•˜๊ณ , FrontEnd ์„œ๋ฒ„๋กœ ๋‹ค์‹œ ๋ณด๋‚ด์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋Š” onSuccess ์ฝœ๋ฐฑ์˜ ๋งค๊ฒŒ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.
 export const useSubmitPlanMutation = () => {
  const queryClient = useQueryClient();
  return useMutation(submitTodayPlanAPI, {
    onSuccess: (_, data) => {
      queryClient.setQueryData<TodayPlan>("today", (prevData: Array<Plan>) => {
        let newData = prevData;
        const findIdx = prevData?.Plans.findIndex((plan: any) => plan?.id === data.id);
        newData.Plans[findIdx].endtime = data.endTime;
        newData.Plans[findIdx].starttime = data.startTime;
        newData.Plans[findIdx].totaltime = data.totaltime;
        return newData;
      });
    },
  });
};

export const useDeletePlanMutation = () => {
  const queryClient = useQueryClient();
  return useMutation(deleteTodayPlanAPI, {
    onSuccess: (response) => {
      queryClient.setQueryData<TodayPlan>("today", (prevData: Array<Plan>) => {
        let newData = prevData;
        newData.Plans = newData?.Plans?.filter((plan: any) => plan?.id !== response);
        return newData;
      });
    },
  });
};

โฑ ๊ณผ๊ฑฐ ๊ธฐ๋ก ํ™•์ธ

  • ๊ณผ๊ฑฐ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ€๊ฒŒ ๋˜๋ฉด, ์‚ฌ์šฉ์ž๋Š” ์ž์‹ ์˜ ๊ณ„ํš๊ณผ ์ˆ˜ํ–‰ ์‹œ๊ฐ„์„ ๋ณด๊ณ  ์‹ถ์€ ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ณด๊ณ  ์‹ถ์€ ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•ด, ์ œ์ถœํ•˜๋ฉด, BackEnd ์„œ๋ฒ„์—์„œ๋Š” ํ•ด๋‹น ๊ธฐ๊ฐ„์— ํฌํ•จ ๋œ ๋‚ ์งœ๋“ค์„ ๊ณ„ํš์„ ํฌํ•จํ•ด ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • ๊ทธ ํ›„, FrontEnd์„œ๋ฒ„์— ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด์ฃผ๋ฉด, FrontEnd ์„œ๋ฒ„๋Š” ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ์—ด๋กœ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•ด์„œ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด, SVG๋ฅผ ํ™œ์šฉํ•ด์„œ ์ฐจํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.
  • ๊ฐ ๋ฐ์ดํ„ฐ์˜ ์ „์ฒด์— ๋Œ€ํ•œ ํผ์„ผํ‹ฐ์ง€๋ฅผ ๊ตฌํ•˜๊ณ , ์ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ SVG Element ์•ˆ์˜ Circle Element๋ฅผ ์ง‘์–ด ๋„ฃ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฐจํŠธ๋ฅผ ๊ทธ๋ ค์ฃผ์—ˆ๋‹ค.
const makeCircle = (data: PlanListDataPercent, chartWrapperRef: React.RefObject<HTMLDivElement>) => {
  let filled = 1;
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  svg.setAttribute("width", "100%");
  svg.setAttribute("height", "100%");
  svg.setAttribute("viewBox", "0 0 100 100");
  if (!chartWrapperRef.current) return;
  chartWrapperRef.current.innerHTML = "";
  chartWrapperRef.current.appendChild(svg);
  data.forEach((o, idx) => {
    const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
    const dashOffset = DASHARRAY - (DASHARRAY * o.percent) / 100;
    const angle = (filled * 360) / 100 + START_ANGLE;
    const currentDuration = (ANIMATION_DURATION * o.percent) / 100;
    const delay = (ANIMATION_DURATION * filled) / 100;
    const attributes = [
      { type: "r", value: RADIUS },
      { type: "cx", value: CX },
      { type: "cy", value: CY },
      { type: "fill", value: "transparent" },
      { type: "stroke", value: COLORS[idx] },
      { type: "stroke-width", value: STROKE_WIDTH },
      { type: "stroke-dasharray", value: DASHARRAY },
      { type: "stroke-dashoffset", value: DASHARRAY },
      { type: "transform", value: `rotate (${angle} ${CX} ${CY})` },
    ];
    attributes.forEach(({ type, value }) => {
      circle.setAttribute(type, String(value));
    });
    circle.style.transition = `stroke-dashoffset ${currentDuration}ms linear ${delay}ms`;
    svg.appendChild(circle);

    filled += o.percent;
    setTimeout(function () {
      circle.style.strokeDashoffset = String(dashOffset);
    }, 100);
  });
};

V3์—์„œ ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ?

  • V3์—์„œ๋„ V2์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์„ ์ ์šฉํ•ด๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. (ex, React v18)
  • V1์— ๋น„ํ•ด V2์—๋Š” ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ์€ ์—†๊ธฐ ๋•Œ๋ฌธ์—, V3์—์„œ๋Š” ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ(ex, ์ผ๊ธฐ, ์šด๋™ ์ผ์ง€ ๋“ฑ๋“ฑ)์„ ๋„ฃ์–ด๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.
  • V3์—์„œ๋Š” Aws Amplify๋ฅผ ํ†ตํ•œ ๋ฐฐํฌ๋ฅผ ๊ฒฝํ—˜ํ•ด ๋ณด๊ณ , Aws Amplify๋ฅผ ํ†ตํ•œ CICD๋ฅผ ๊ตฌ์ถ•ํ•ด๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.