๐ 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);
}, []);
- ์ฒซ ๋จ๊ณ์์๋ ์ฌ์ฉ์๊ฐ ์ํ๋ ๋ ์ง๋ฅผ ์ ํํ๊ฒ ๋ฉ๋๋ค.
- ์ํ๋ ๋ ์ง๋ฅผ ์ ํํ ํ, 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);
- ๋๋ฒ์งธ ๋จ๊ณ์์๋ ์ฌ์ฉ์๊ฐ ์ ํํ ๋ ์ง์ ์ํ๋ ๊ณํ์ ์ค์ ํฉ๋๋ค.
- โ ๋ฒํผ๊ณผ โ ๋ฒํผ์ ์ด์ฉํด ์ํ๋ ๊ณํ์ ์ ๋ ฅํ๋ ๊ณต๊ฐ์ ๋์ ์ผ๋ก ์กฐ์ ํ ์ ์์ต๋๋ค.
- ๊ณํ์ ์ ๋ ฅํ ํ "๋ฑ๋ก" ๋ฒํผ์ ๋๋ฅด๋ฉด ํด๋น ๊ณํ์ด 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]);
- ๋ชจ๋ ์ค์ ์ด ์๋ฃ๋๋ฉด, ์ค์ ์๋ฃ ํ์ด์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ณ , ์ฌ์ฉ์๋ฅผ ๋ฉ์ธํ์ด์ง๋ก ๋๋ ค๋ณด๋ ๋๋ค.
- ์ค๋์ผ์ ์ ๋ค์ด๊ฐ๋ฉด, ์ฌ์ฉ์๊ฐ ์ด ์ ์ ์ค์ ํด๋ ๋น์ผ์ ๊ณํ๋ค์ด ๋ค์๊ณผ ๊ฐ์ 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์์๋ V2์ ๋ง์ฐฌ๊ฐ์ง๋ก ์๋ก์ด ๊ธฐ์ ์ ์ ์ฉํด๋ณด๊ณ ์ถ์ต๋๋ค. (ex, React v18)
- V1์ ๋นํด V2์๋ ์๋กญ๊ฒ ์ถ๊ฐ๋ ๊ธฐ๋ฅ์ ์๊ธฐ ๋๋ฌธ์, V3์์๋ ์๋ก์ด ๊ธฐ๋ฅ(ex, ์ผ๊ธฐ, ์ด๋ ์ผ์ง ๋ฑ๋ฑ)์ ๋ฃ์ด๋ณด๊ณ ์ถ์ต๋๋ค.
- V3์์๋ Aws Amplify๋ฅผ ํตํ ๋ฐฐํฌ๋ฅผ ๊ฒฝํํด ๋ณด๊ณ , Aws Amplify๋ฅผ ํตํ CICD๋ฅผ ๊ตฌ์ถํด๋ณด๊ณ ์ถ์ต๋๋ค.