230927(์) ๋ฒ ์คํธ๋ผ๋น์ค 1์ฐจ ํ๊ณ
๐จ ๋ฒ ์คํธ๋ผ๋น์ค ํ๋ก์ ํธ
์ฝ 10์ผ๋์ ๊ธฐํ๋ถํฐ ๋์์ธ, ๊ฐ๋ฐ๊น์ง ์งํํ ๋ฒ ์คํธ๋ผ๋น์ค ํ๋ก์ ํธ. ๋๋ฃ์๊ฒ ์ถ์ฒ๋ฐ์ ์๊ฒ๋ ํฌํ ๋ฐ์ด๋ฅผ ํตํด ์ง์ํ๊ณ ์ข์ ๊ธฐํ์๋ถ๊ณผ ๋์์ด๋๋ถ์ ๋ง๋์ ์ด์ฌํ ๋ฌ๋ ค๋ณผ ์ ์์๋ค. ์ฌ์ค ๋งค์ผ๋งค์ผ ํ๊ณ ๋ฅผ ์์ฑํ๋ คํ๋๋ฐ, 10์ผ์ด๋ผ๋ ์งง์ ์๊ฐ๋์ ํ๋์ ์๋น์ค๋ฅผ MVP๊น์ง ์์ฑํ๋ ๊ฒ ์ฌ์ด ์ผ์ด ์๋์๋ค. ํด์ ์ฐ๋ฆฌ ํ์ด 10์ผ ๋์ ์ด๋ค ๊ณผ์ ์ ๊ฑฐ์ณ์ ํ๋ก์ ํธ๋ฅผ ์งํํ๋์ง, ๊ทธ๋ฆฌ๊ณ ๋๋ ์ด๋ค ์ญํ ์ ๋งก์์ ์ด๋ค ๊ณต๋ถ๋ฅผ ํ๋์ง์ ๋ํด ํ๊ณ ํด๋ณด๋ ค๊ณ ํ๋ค.(1์ฐจ ํ๊ณ ์ธ ์ด์ ๋ ์ดํ์๋ ๋ช๋ช ๊ธฐ๋ฅ์ ๋ํ๊ณ ๋ฆฌํฉํ ๋ง์ ์งํํ ์์ ์ด๊ธฐ ๋๋ฌธ์ด๋ค!)
๋ฒ ์คํธ ๋ผ๋น์ค: ๋ฐฐํฌ ๋งํฌ
1. ์์ด๋์ด ํ์ ๋ฐ ํ ๋ชฉํ ์ค์
๊ฐ์ ์ด๋ค ์๋น์ค๋ฅผ ํ๊ณ ์ถ์์ง ์๊ฐํด์ค๊ธฐ๋ก ํ๋ค. ์ฌ๋ฌ๊ฐ์ง ์์ด๋์ด๋ค ์ค ์ฐ์ 10์ผ ์์ MVP ๊ธฐํ ๋ฐ ๋์์ธ, ๊ฐ๋ฐ์ด ๊ฐ๋ฅํ๊ณ ์ถํ ํ์ฅ์ด ๊ฐ๋ฅํ ์์ดํ
์ ๊ณ ๋ฅด๊ธฐ๋ก ํ๊ณ ๊ทธ ๊ฒฐ๊ณผ ์ฌ๋๋ค์๊ฒ ๋ฐฐ์คํจ ๋ผ๋น์ค ์์ด์คํฌ๋ฆผ ์กฐํฉ์ ์ถ์ฒํด์ฃผ๋ ์๋น์ค
๋ฅผ ๊ฐ๋ฐํ๊ธฐ๋ก ํ๋ค. ๊ตฌ์ฒด์ ์ธ ๊ธฐํ๊ณผ ๋์์ธ, ์ถ์ฒ ๋ฐฉ๋ฒ์ ์ ํด์ง์ง ์์์ง๋ง, ์ฐ์ ์ ์ผ๋ก 10์ผ ์์ MVP๋ฅผ ์์ฑํ๋ ๊ฒ์ ์ต์ฐ์ ์ผ๋ก ํ๊ธฐ๋ก ํ๋ค.
2. ๊ธฐํ ๋ฐ ๋์์ธ
๊ฐ์ ๋ณธ์ธ์ด ์๊ฐํ๋ ์ฐ๋ฆฌ ์๋น์ค์ ๊ธฐํ๊ณผ ๋ชจ์ต์ ์๊ฐํ๊ณ ๊ฐ์ ธ์ค๊ธฐ๋ก ํ๋ค! ๋ ๊ฐ์ ๊ฒฝ์ฐ, ์์ด์คํฌ๋ฆผ ๋ชฉ๋ก์ ๋ณด์ฌ์ฃผ๊ณ ์ ์ ๊ฐ ์ ํ์ ํ๋ฉด ํด๋น ์์ด์คํฌ๋ฆผ๋ค์ ์กฐํฉ์ ๋ฅ๋ ฅ์น(?)๋ฅผ ๋ณด์ฌ์ค์ผ๋ก์จ ํด๋น ์กฐํฉ์ ์ถ์ฒํ๋์ง ๊ทธ๋ ์ง ์์์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ์๋น์ค๋ฅผ ์๊ฐํ๋ค. ํ์ง๋ง ๊ทธ๊ฒ๋ณด๋ค๋ ๋ฐฐ์คํจ ๋ผ๋น์ค๋ฅผ ๋ชฐ๋ผ๋ ์๋น์ค๋ฅผ ์ด์ฉํ ์ ์๊ฒ ์ฌ๋ฃ๋ฅผ ์ ํํ๋ฉด ํด๋น ์ฌ๋ฃ๊ฐ ํฌํจ๋ ์์ด์คํฌ๋ฆผ ์กฐํฉ์ ์ถ์ฒํด์ฃผ์๋ ์๊ฒฌ์ด ๋์๊ณ ๊ทธ๋ ๊ฒ ์๋น์ค๋ฅผ ๋ง๋ค๊ธฐ๋ก ํ๋ค. ์๋๋ ๋ด ๋๋ฆ ์ด์ฌํ ๋ง๋ ์์ด์ดํ๋ ์์ด๋ค!
๊ทธ๋ฆฌ๊ณ ์๋๋ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ์ ์ฅ์์ ์์ด์ด ํ๋ ์์ ๋ฐ๋ผ ์์ฑํด๋ณธ ์ ์ ํ๋ก์ฐ์ด๋ค.
3. ๊ฐ๋ฐ
๊ฐ๋ฐ ํฌ์ธํธ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๋์์ด๋๋ถ๊ป์ ์ค์๊ฐ์ผ๋ก ์์
ํด์ฃผ์๋ ํผ๊ทธ๋ง๋ฅผ ๋ณด๋ฉด์ ๊ฐ๋ฐ์ ๋ค์ด๊ฐ๋ค! ์ด๋ฒ ์๋น์ค์์ ๊ฐ๋ฐ์ ์ผ๋ก ๊ฐ์ฅ ์ค์ํ๊ฒ ์๊ฐํ๋ ๋ถ๋ถ์ ๋จ์ํ ์น์ผ๋ก ๊ทธ์น์ง ์๊ณ PWA(Progressive Web App) ํํ์ ์๋น์ค๋ฅผ ๋ฐฐํฌํ๋ ๊ฒ์ด์๋ค. ์์ฃผ ๋๋จํ ๋ฌด์ธ๊ฐ๋ฅผ ํ๋ ๊ฑด ์๋์ง๋ง, pwa๊ฐ ๋ฌด์์ธ์ง ๊ณต๋ถํ ์ ์์๊ณ ํ์์๋ ๊ฑฐ์ ์ ๊ฒฝ๋ ์ฐ์ง ์์๋ html์ head
ํ๊ทธ์ ๋ํด์๋ ์์ฐ์ค๋ฝ๊ฒ ์ ๊ฒฝ์ธ ์ ์์๋ค. ๊ทธ ๋ค์์ผ๋ก๋ ์ฝ๋์ ์ฌํ์ฉ ๋ฐ ๊ท์น์ฑ์ ์ ๊ฒฝ์ฐ๋ ค๊ณ ํ๋ค. ๋๋ฌด ์๊ณ ์ผ๋ฐํ๋ ์ปดํฌ๋ํธ๊ฐ ์๋๋ผ ์ฃผ์ด์ง ์งง์ ์๊ฐ ์์(๊ฐ๋ฐ ์๊ฐ ์์ฒด๋ ๊ฑฐ์ 4~5์ผ) ์ต๋ํ ํจ์จ์ ์ผ๋ก ์ฌํ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ๋ค์ ์์ฑํ๋ ค ํ๋ค.
๋จผ์ PWA์ ๋ํ ๋ด์ฉ์ ์ฌ๊ธฐ์ ๊ฐ๋จํ ์์ฑํด๋์๊ณ ์ฐธ๊ณ ์๋ฃ๋ ๋์๋ค.
๊ธฐ์ ์คํ
์ฌ์ค ์ฒ์์ ์์ฆ ์ด์ฌํ ๋ฐฐ์ฐ๊ณ ์๋ Next
๋ฅผ ์ฌ์ฉํด๋ณด๋ ค ํ๋ค. ํ์ง๋ง ์ง๊ธ ํ๋ก์ ํธ์ ๊ท๋ชจ์์ ๊ตณ์ด Next๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์์ ๊ฒ ๊ฐ์๊ณ , ๋๋ฌด ์งง์ ์๊ฐ์ด๋ผ ๋ด๊ฐ ์ต์ํ React๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ๋์ ๊ฒ ๊ฐ๋ค๋ ํ๋จ ํ์ React๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ฒ์์ ์์ข์ํ์ง๋ง, ์ง๊ธ์ ๋๋ฌด ํธํ๊ฒ ์ ์ฐ๋ ์ค์ธ styled-components, ์ธ์ ์จ๋ ์ฐธ ์ ๋ง๋ค์๋ค๊ณ ์๊ฐ์ด ๋๋ react-query๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ฐ๋ฐํ๊ธฐ๋ก ํ๋ค. (์ด์ธ์๋ axios, react-router-dom ๋ฑ์ ์ฌ์ฉํ๋ค.)
๊ฐ๋ฐ ๊ณผ์
๋ธ๋์น ์ ๋ต
ํโฆ ๋ธ๋์น ์ ๋ต๋ ์ ๋ง ํ ๋ง์ด ๋ง๋ค. ์ฒ์์ ๋ณดํต ์ด์ ์ ํ๋ ํ ํ์
๊ณผ ๋น์ทํ๊ฒ main
์ผ๋ก ๋ฐฐํฌํ๊ณ , develop
์ ๊ฐ๋ฐ ๊ธฐ๋ฅ๋ค์ ๋ชจ์ผ๊ณ ๊ฐ ๊ธฐ๋ฅ ๋ณ๋ก ๋ธ๋์น๋ฅผ ๋ฐ์ ์์
ํ๋ค. ํ์ง๋ง FE ๊ฐ๋ฐ์๋ ๋ ํผ์์ธ ์ํ์์ ์ฝ๋๋ฆฌ๋ทฐ๋ ์๋ ํ๊ฒฝ์ ๋งค๋ฒ ๊ธฐ๋ฅ๋ค๋ง๋ค ๋ธ๋์น๋ฅผ ๋ฐ๊ณ ํผ์ ํธ์ฌํ๊ณ ๋จธ์งํ๋ ๊ฑด ์๊ฐ๋ณด๋ค ๋นํจ์จ์ ์ด๊ณ ์๋ฏธ๊ฐ ์๋ค๊ณ ๋๊ผ๋ค. ํด์ develop ๋ธ๋์น์์ ๋ชจ๋ ๊ฐ๋ฐ์ ์งํํ๋ ์ปค๋ฐ ๋ฉ์์ง๋ฅผ ์์ฃผ ์๊ฒ์๊ฒ ์์ฑํ์ฌ ์ต๋ํ ๊ธฐ๋ฅ ๋ณ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๊ณ ์ ํ๋ค. ์ด ๋ฐฉ๋ฒ์ด ์์ฃผ ์ข์ ๋ฐฉ๋ฒ์ ์๋์ง๋ง, ํผ์ ๋น ๋ฅด๊ฒ ์น๊ณ ๋๊ฐ๊ธฐ์ ๊ฐ์ฅ ํจ์จ์ ์ธ ๋ฐฉ๋ฒ๊ฐ๋ค.
๊ฐ๋ฐ ์์
์ฒ ์ ํ ํ์ด์ง ๋จ์๋ก ์์ ์ ํด๋ณด๊ณ ์ ํ๋ค. ์ฌ์ค ๊ฐ๋ฐ์ ํ ๋๋ง๋ค, ์ด๋ค ๊ธฐ์ค์์ ๋ญ๋ถํฐ ํด์ผํ ์ง ๋ง๋งํ ๋๊ฐ ์ข ์ข ์๋๋ฐ, ์ด๋ฒ์ ์ ๋ง ํ์คํ๊ฒ ๊ทธ ์ฐ์ ์์๋ฅผ ์ ํ๋ ค๊ณ ํ๋ค. ๊ฐ๋ฅํ๋ฉด ๋ชจ๋ ๊ธฐ์ ์คํ๊ณผ ๊ธ๋ก๋ฒ ์ค์ ๋ค์ ์ฒ์์ ๋ค ์ก์๋์๊ณ (๋ผ์ฐํฐ ํฌํจ) ์ดํ์ ์ฐ๋ฆฌ ์๋น์ค์์ ์ค์ํ ํ์ด์ง๋ถํฐ ์ณ๋ด๊ฐ๋ฉด์ UI ์ปดํฌ๋ํธ๋ฅผ ๋น ๋ฅด๊ฒ ๊ฐ๋ฐํ๋ค. ์ด ๋, ์คํ ๋ฆฌ๋ถ์ ์ฌ์ฉํด์ ์์ ํ๋ ๊ฒ ์ ๋ง ํฐ ๋์์ด ๋ง์ด ๋์๋ค.(์ UI ํ ์คํ ํด์ด๋ผ๊ณ ๋ ๋ถ๋ฅด๋์ง ์๊ฒ ๋๋ผ..!)
ํ์ง๋ง ์ค๊ฐ์ ํ์ด์ง ์ปดํฌ๋ํธ๋ค์ด router ์ค์ , react-query์ ์ถฉ๋ํ๋ ๋ถ๋ถ๋ค์ด ์์ด์ ์คํ ๋ฆฌ ์์ฑ์ ์ค๊ฐ์ ๋ฉ์ท๋ค. ์๊ฐ๋ง ์ข๋ ์๋ค๋ฉด ํด๋น ๋ด์ฉ์ ํด๊ฒฐํ๊ณ ์ถ์๋ฐ, ๋น์ฅ ๋น ๋ฅด๊ฒ ๊ฐ๋ฐํด์ผํ๋ ์ํฉ์์๋ ๊ณผ๊ฐํ๊ฒ ์ ํ๊ณผ ์ง์ค์ ํ๋ ๊ฒ ๋์์ด ๋ ๊ฑฐ๋ผ ์๊ฐํ๊ธฐ ๋๋ฌธ์ด๋ค.
๊ฐ๋ฐ ๋ด์ฉ
1. ์บ๋ก์ ์ปดํฌ๋ํธ
์ฐ๋ฆฌ ์๋น์ค์ ๋ฉ์ธ ํ์ด์ง์ ๋ค์ด๊ฐ ์บ๋ก์ ์ฌ๋ผ์ด๋ ์ปดํฌ๋ํธ์ด๋ค. ์ฒ์์ ๋น์ฐํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ ธ๋ค ์ฐ๋ ค๊ณ ํ๋ค.
์์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค๋ง๊ณ ๋ UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Material, Chakra, Mantine, Ant ๋ฑ๋ฑ)๋ ๊ณ ๋ คํด๋ณด์์ง๋ง, ์ ์ฉํด๋ณด๋ฉด UI๊ฐ ๊นจ์ง๋ค๋ ๊ฐ ์ฌ๋ผ์ด๋๊ฐ ์ ๋๋ก ๋์ง ์๋๋ค๋ ๊ฐโฆ ํน์ ๋ด๊ฐ๊ฐ ์ํ๋ ์บ๋ก์ ์ด ์๋๋ ๊ฐ ๋ค์ํ ๋ฌธ์ ๊ฐ ํ๋์ฉ์ ์์๋ค. ์ฐธโฆ ์ฌ๋์ด ์๊ฐ์ด ์์ผ๋ฉด ์ด๋ป๊ฒ๋ ํ๋ค๊ณ , ๋น ๋ฅด๊ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ํฌ๊ธฐํ๊ณ (ํผ์ ๊ฑฐ์ ํ๋ฃจ์ข ์ผ ์ธ๋งค๋ค๊ฐ ๋๋ฌผ์ ๋จธ๊ธ๊ณ ํฌ๊ธฐํจ) ์ง์ ์บ๋ก์ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๊ธฐ๋ก ํ๋ค. ์ด์ ์ ์งํํ๋ ๋น๊ทผ๋ง์ผ ํด๋ก ํ๋ก์ ํธ์์ ์ฌ๋ผ์ด๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํด๋ณธ ๊ฒฝํ์ด ์์ด์ ์กฐ๊ธ ์ต์ํ๊ฒ ์์ฑํ ์ ์์๋ค.
import React, { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';
import { isMobile } from '../../utils/isMobile';
import { ReactComponent as ButtonLeft } from '../../assets/icons/common/chevron_left.svg';
import { ReactComponent as ButtonRight } from '../../assets/icons/common/chevron_right.svg';
import * as S from './Carousel.styled';
type CarouselProps = {
children: React.ReactNode;
currentSlideIndex: number;
changeSlide: (index: number) => void;
};
type SlideProps = {
children: React.ReactNode;
isCurrentSlide?: boolean;
};
const Carousel = ({ children, currentSlideIndex, changeSlide }: CarouselProps) => {
const touchStartPositionX = useRef(0);
const touchEndPositionX = useRef(0);
const handleSlideClick = (index: number) => {
changeSlide(index);
};
useEffect(() => {
const timer = setInterval(() => {
if (currentSlideIndex >= React.Children.count(children) - 1) {
changeSlide(0);
return;
}
changeSlide(currentSlideIndex + 1);
}, 4000);
return () => {
clearInterval(timer);
};
}, [currentSlideIndex, children]);
const theme = useTheme();
return (
<S.CarouselContainer>
<S.SlideContainer
$currentSlideIndex={currentSlideIndex}
onTouchStart={(e) => {
touchStartPositionX.current = e.touches[0].clientX;
}}
onTouchEnd={(e) => {
touchEndPositionX.current = e.changedTouches[0].clientX;
const touchPositionDifference = touchStartPositionX.current - touchEndPositionX.current;
if (touchPositionDifference > 5) {
if (currentSlideIndex >= React.Children.count(children) - 1) {
return;
}
handleSlideClick(currentSlideIndex + 1);
}
if (touchPositionDifference < -5) {
if (currentSlideIndex <= 0) {
return;
}
handleSlideClick(currentSlideIndex - 1);
}
}}
>
{children}
</S.SlideContainer>
{isMobile() || (
<>
{currentSlideIndex <= 0 || (
<S.ArrowLeftButton
onClick={() => {
if (currentSlideIndex <= 0) {
return;
}
handleSlideClick(currentSlideIndex - 1);
}}
>
<ButtonLeft fill={theme.colors.gray_05} />
</S.ArrowLeftButton>
)}
{currentSlideIndex >= React.Children.count(children) - 1 || (
<S.ArrowRightButton
onClick={() => {
if (currentSlideIndex >= React.Children.count(children) - 1) {
return;
}
handleSlideClick(currentSlideIndex + 1);
}}
>
<ButtonRight fill={theme.colors.gray_05} />
</S.ArrowRightButton>
)}
</>
)}
</S.CarouselContainer>
);
};
const Slide = ({ children, isCurrentSlide }: SlideProps) => {
return <S.SlideWrapper $isCurrentSlide={isCurrentSlide}>{children}</S.SlideWrapper>;
};
Carousel.Slide = Slide;
export default Carousel;
์ฝ๋๊ฐ ๋ค์ ๊ธธ์ง๋ง, ์ฐ์ ์ฌ๋ผ์ด๋๋ฅผ ๋ด๊ณ ์๋ Carousel ์ปดํฌ๋ํธ์ ๊ทธ ๋ด๋ถ์ Slide ์์ฑ์ผ๋ก ์กด์ฌํ๋ Slide ์ปดํฌ๋ํธ๊ฐ ์๋๋ก ๊ฐ๋ฐํ๋ค.(์ง๊ธ ๋ค์ ๋ณด๋ 4000๊ณผ ๊ฐ์ ๋งค์ง ๋๋ฒ๊ฐ ์กด์ฌํ๋คโฆ ํํฃ) ๋, ํฐ์นํ ์๊ฐ๋ถํฐ ๋ผ๊ธฐ๊น์ง์ ๊ฑฐ๋ฆฌ๊ฐ 5px ์ด์ ์ฐจ์ด๊ฐ ๋๋ฉด ์ฌ๋ผ์ด๋๋ฅผ ๋๊ธฐ๋๋ก ๊ตฌํํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฌธ์ ๋ ์น์์๋ touch ์ด๋ฒคํธ ์์ฒด๊ฐ ๋์ง ์๊ธฐ ๋๋ฌธ์ isMobile()
์ด๋ผ๋ ์ ํธํจ์๋ก ํ์ฌ ์ ์ ์ ์คํ ํ๊ฒฝ์ ๋ฐ๋ผ ์บ๋ก์
ํ์ดํ๋ฅผ ๋ ๋๋งํ๋๋ก ํ๋ค.(๋ฐ์คํฌํ์์ ๋ณผ ๋๋ ์ ์์ ํ์ดํ๋ฅผ ํด๋ฆญํ์ฌ ์ฌ๋ผ์ด๋๋ฅผ ์์ง์ผ ์ ์๋๋ก!)
2. react-query service
API ํต์ ์ ๋ํ ์ค์ ์ api
ํด๋์์ ์์ฑํ๊ณ ๊ฐ api ์์ฒญ์ ๋ํ ํจ์๋ค์ service
ํด๋์ ๋ถ๋ฆฌํ์ฌ ์์ฑํ์๋ค. ์๋ฅผ ๋ค๋ฉด ์๋์ ๊ฐ๋ค.
api/apiConfig.ts
export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
api/apiClient.ts
import axios from 'axios';
import { API_BASE_URL } from './apiConfig';
export const axiosFetch = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
service/useGetRecipe.ts
import { useQuery } from '@tanstack/react-query';
import { axiosFetch } from '../../api/apiClient';
type Flavor = {
id: number;
flavorName: string;
imageUrl: string;
};
export type Recipe = {
id: number;
recipeName: string;
flavors: Flavor[];
};
export const useGetRecipe = (id: number) => {
return useQuery<{ recipe: Recipe; message: string }>(['recipe', id], async () => {
const { data } = await axiosFetch(`/recipes/${id}`);
const { body: recipe, message } = data;
return { recipe, message };
});
};
์์ ๊ฐ์ ์์ผ๋ก api ํต์ ์ ๋ํ ์ค์ ๊ณผ ์ค์ api ์์ฒญ ํจ์๋ฅผ ๋ถ๋ฆฌํ์ฌ ์์ฑํ์๋ค. ์ด๋ ๊ฒ ๋ถ๋ฆฌํ๋ฉด ์ถํ์ api ํต์ ์ ๋ํ ์ค์ ์ ๋ณ๊ฒฝํ๊ฑฐ๋, api ์์ฒญ ํจ์๋ฅผ ๋ณ๊ฒฝํ ๋์๋ ํธ๋ฆฌํ๊ฒ ์์ ํ ์ ์๋ค.
3. Page ์ปดํฌ๋ํธ
๊ฐ๋ฐ ์ด๊ธฐ์ ์ต๋ํ Page ์ปดํฌ๋ํธ๋ค์ api ํต์ ํ๋ ๋ก์ง๊ณผ ํ์ด์ง์ ๋ํ ๋ ์ด์์์ ์ก๋ ์ฉ๋๋ก๋ง ์กด์ฌํ๊ฒ ํ๋ ค ํ๋ค. ๊ทธ๋ฐ๋ฐ ๊ฐ๋ฐ์ ํ๋ค๋ณด๋ ์๊ฐ๋ณด๋ค ์ํ ์์ฒด๋ฅผ ๋์ด์ฌ๋ ค์ผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์ก๊ณ ๊ทธ๋ฌ๋ค๋ณด๋ Page ์ปดํฌ๋ํธ๋ค์ ์กด์ฌํ๋ ์ฑ ์๊ณ ์ญํ ์ด ๋๋ฌด ๋ง์์ง๊ฒ ๋์๋คโฆ depth๊ฐ ๊ทธ๋ ๊ธฐ ๊น์ง ์์ ์ํ๋ค์ ๊ตณ์ด Context API๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ ค๋ค๋ณด๋๊น ์ด๋ฐ ์ผ์ด ์๊ธด ๊ฒ ๊ฐ๋ค.
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { routes } from '../../router';
import SubGlobalNavBar from '../../components/SubGlobalNavBar';
import FootButton from '../../components/FootButton';
import Text from '../../components/Text';
import { PROGRESS_BAR_WIDTH } from '../../constants';
import { useGetIngredients } from '../../services/useGetIngredients';
import { usePostRecipe } from '../../services/usePostRecipe';
import { useUserItem, useChangeIngredientIds } from '../../contexts/UserItemContext';
import LoadingPage from '../LoadingPage';
import * as S from './IngredientSelectPage.styled';
const IngredientSelectPage = () => {
const { data, isLoading } = useGetIngredients();
const { size, ingredientIds } = useUserItem();
const changeIngredientIds = useChangeIngredientIds();
const mutation = usePostRecipe();
const allFlavorIdList = ingredientIds
.map((id) => {
const flavorIdList = data?.ingredients?.find((ingredient) => ingredient.id === id)
?.flavorIdList;
return flavorIdList;
})
.flat();
const flavorIdList = allFlavorIdList.filter(
(item, index) => allFlavorIdList.indexOf(item) === index,
);
const isAbleToRecommend = flavorIdList.length >= size.value;
const navigate = useNavigate();
if (size.id === -1) {
navigate(routes.sizePick);
}
return (
<>
{isLoading && <LoadingPage />}
{data && (
<S.Container>
<S.UpperContainer>
<S.Header>
<SubGlobalNavBar backTo={routes.sizePick} progressWidth={PROGRESS_BAR_WIDTH.MIDDLE} />
</S.Header>
<S.Main>
<Text size="large">์ข์ํ๋ ์ฌ๋ฃ๋ฅผ ์ ํํด์ฃผ์ธ์</Text>
<S.IngredientsContainer>
{data?.ingredients?.map((ingredient) => (
<S.IngredientWrapper
key={ingredient.id}
onClick={() => {
if (ingredientIds.includes(ingredient.id)) {
changeIngredientIds(ingredientIds.filter((id) => id !== ingredient.id));
} else {
changeIngredientIds([...ingredientIds, ingredient.id]);
}
}}
>
<S.IngredientImage
src={ingredient.imageUrl}
alt={ingredient.name}
$isClicked={ingredientIds.includes(ingredient.id)}
/>
<S.IngredientName>{ingredient.name}</S.IngredientName>
</S.IngredientWrapper>
))}
</S.IngredientsContainer>
</S.Main>
</S.UpperContainer>
<S.Footer>
<S.BottomContainer>
{isAbleToRecommend || ingredientIds.length === 0 || (
<Text size="small">์ฌ๋ฃ๋ฅผ ์กฐ๊ธ ๋ ๊ณจ๋ผ๋ณผ๊น์?</Text>
)}
<FootButton
onClick={() => {
mutation.mutate({
sizeId: size.id,
ingredientIds,
});
}}
disabled={!isAbleToRecommend}
>
๊ฒฐ๊ณผ๋ณด๊ธฐ
</FootButton>
</S.BottomContainer>
</S.Footer>
</S.Container>
)}
</>
);
};
export default IngredientSelectPage;
4. Context API์ useState
์ํฐ๋ ์ธํด์ญ์ ํตํด ๋ฐฐ์ด ๊ฒ ์์๋ค. Context API๋ ์ ์ญ์ํ ๊ด๋ฆฌ๋ฅผ ์ํ api๊ฐ ์๋๋ผ๋ ๊ฒ์ด๋ค. ์ ํํ ๋งํ๋ฉด ์ปดํฌ๋ํธ๋ค์ ์งํฉ์ ์ด๋ค ๋ฌธ๋งฅ(Context)์ ์ ๋ฌํ๋ ์ฉ๋์ด์ง, ๊ทธ ํผ์์๋ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ํ ์ ์๋ค๋ ์๋ฏธ์๋ค. ์ฒ์์ ์ ์ญ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๊น ํ๋ค๊ฐ, ๋ ๋๋ง ํจ์จ์ด๋ ์ต์ ํ๊ฐ ์ข์ง ์๋๋ผ๋ Context API์ useState๋ฅผ ํตํ ์ํ๊ด๋ฆฌ๋ฅผ ์ง์ ๋๊ปด๋ณด๊ณ ์ถ์ด์ Context API์ useState๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ์งํํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ฒ ํ๋ก์ ํธ๋ฅผ ํตํด Context API์ useState๋ฅผ ์ฌ์ฉํ ์ ์ญ ์ํ ๊ด๋ฆฌ๊ฐ ์ด๋ค ์์ผ๋ก ์ด๋ฃจ์ด์ง๋์ง ๋ง์ด ๋ฐฐ์ธ ์ ์์๋ค.(์ฝ๋๊ฐ ์ง์ ๋ถํ๊ธด ํ๋ค.)
์๋๋ ์ ์ ๊ฐ ์ ํํ ๊ฐ๋ค์ ์ ์ญ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํ Context ์ฝ๋์ด๋ค.
import React, { createContext, useContext, useState } from 'react';
type Size = {
id: number;
value: number;
};
type UserItem = {
size: Size;
ingredientIds: number[];
};
export const userItemContext = createContext<UserItem>({
size: { id: -1, value: -1 },
ingredientIds: [],
});
export const changeSizeContext = createContext(({ id, value }: Size) => {
console.error(`changeSizeContext๋ฅผ ๋ฒ์ด๋ฌ์ต๋๋ค. size: {id: ${id}, value: ${value}}`);
});
export const changeIngredientIdsContext = createContext((ingredientIds: number[]) => {
console.error(`changeIngredientIdsContext๋ฅผ ๋ฒ์ด๋ฌ์ต๋๋ค. ingredientIds: ${ingredientIds}`);
});
export const initUserItemContext = createContext(() => {
console.error('initUserItemContext๋ฅผ ๋ฒ์ด๋ฌ์ต๋๋ค.');
});
export const useUserItem = () => useContext(userItemContext);
export const useChangeSize = () => useContext(changeSizeContext);
export const useChangeIngredientIds = () => useContext(changeIngredientIdsContext);
export const useInitUserItem = () => useContext(initUserItemContext);
type Props = {
children: React.ReactNode;
};
export const UserItemProvider = ({ children }: Props) => {
const [userItem, setUserItem] = useState<UserItem>({
size: { id: -1, value: -1 },
ingredientIds: [],
});
const changeSize = ({ id, value }: Size) => {
setUserItem({
...userItem,
size: { id, value },
});
};
const changeIngredientIds = (ingredientIds: number[]) => {
setUserItem({
...userItem,
ingredientIds,
});
};
const initUserItem = () => {
setUserItem({
size: { id: -1, value: -1 },
ingredientIds: [],
});
};
return (
<userItemContext.Provider value={userItem}>
<changeSizeContext.Provider value={changeSize}>
<changeIngredientIdsContext.Provider value={changeIngredientIds}>
<initUserItemContext.Provider value={initUserItem}>
{children}
</initUserItemContext.Provider>
</changeIngredientIdsContext.Provider>
</changeSizeContext.Provider>
</userItemContext.Provider>
);
};
4. ๋ง๋ฌด๋ฆฌ
์ ๋ง ํญํ๊ฐ์ 10์ผ์ด ์ง๋๊ฐ๋ค. ์์งํ ์ด๋ฐ ๊ธฐํ๊ณผ ๋์์ธ ๋๋ ์๊ฐ๋ณด๋ค ๋ฐ์์ง ์์ ๋๋์ด์๋๋ฐ, ํ๋ฐ๋ถ์ ๊ฐ๋ฐ์ ํ๋ฉด์ ์ ๋ง ๋ฐ์๊ฒ ์ง๋๊ฐ๋ค. ํฌ๊ฒ ์ค๋ ๊ฑธ๋ฆฌ์ง ์์๊ฑฐ๋ผ ์๊ฐํ๋ UI์ ๊ธฐ๋ฅ๋ค์ด ๋ด๊ฐ ์๊ฐํ ๊ฒ๋ณด๋จ ์๊ฐ์ด ๋ ๊ฑธ๋ ธ๋ค. ์ด๋์ ํ์ ์์ ๋ณธ์ธ์ด ์๊ฐํ๋ ๊ฐ๋ฐ ์๊ฐ๋ณด๋ค 1.5๋ฐฐ ์ ๋๋ ๋ ๊ฑธ๋ฆฐ๋ค๊ณ ์๊ฐํ๋ผ๋๊ฑฐ๊ตฌ๋ ๋๋ ์ ์์๋ค.ใ ใ ๊ธฐํ์์ ํจ๊ป ๊ธฐ๋ฅ์ ๋ ผ์ํ๊ณ ์๊ฐ ๋ด์ ๊ฐ๋ฅํ ๊ธฐ๋ฅ๋ถํฐ ๋ค์ ํ๋ค ๊ฑฐ ๊ฐ์ ๋ถ๋ถ์ ์ด์ผ๊ธฐํ๋ ๊ณผ์ , ๋์์ด๋์ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ UI๋ฅผ ์กฐ์ ํ๋ ๊ฒฝํ ๋ฑ์ด ์คํธ๋ ์ค๊ฐ ์๋๋ผ๋ฉด ๊ฑฐ์ง๋ง์ด์ง๋ง ๊ต์ฅํ ์ ์ตํ๋ค. ํ์ ์์๋ ์ด๋ณด๋ค ๋ ํฐ ๋ถ๋ด์ด ์๊ฒ ์ง๋ง, ๋ง์ ๋ค์ด๋ฅ์น๋ฉด ๋ ์ด๋ป๊ฒ๋ ํด๋ผ ์ฌ๋์ด๋ผ๋ ๊ฑธ ์๊ธฐ ๋๋ฌธ์ ๋ ์ด์ฌํ ์ํ ์ ์์ ๊ฒ์ด๋ค. ๋, ์์ง์ ๋ง์ด ๋ถ์กฑํ์ง๋ง ์ง์์ ์ผ๋ก ๋ฐฐํฌ๋ฅผ ํ๊ณ GA๋ ๋ถ์ฌ์ ์ ๋ง ์๋น์ค๋ฅผ ์ด์ํด๋ณด๋ ๊ฒฝํ์ ํ ์๊ฐ์ด๋ค. ์ฌ์ค ์ด๋ฒ์๋ ์๋ง์ ์ฌ์ฉ์ ํผ๋๋ฐฑ๋ค์ด ๋ค์ด์์ ์ด๋ฅผ ์ด๋ป๊ฒ ๋ฐ์ํ๊ณ ๊ณ ์ณ๋ณผ๊น ๊ณ ๋ฏผ์ค์ด๋ค. ์ด๋ฐ ๋ด์ฉ๋ค์ ๋ฐํ์ผ๋ก ์๋์ ๊ฐ์ ๊ฒ๋ค์ ํ๋ ค๊ณ ํ๋ค.
๋ก๊ทธ์ธ(์นด์นด์ค ํน์ ๋ค์ด๋ฒ)
๋ถ๋งํฌ ๊ธฐ๋ฅ
์จ๋ณด๋ฉ ํ์ด์ง(๋ถ๋งํฌ๋ฅผ ํตํ ์ฑ ๊ฒฝํ ์๋ด๊ฐ ๋ ๊ฒ ๊ฐ์์)
์ฃผ์ ๋ฐฐ์คํจ๋ผ๋น์ค ์์น ์ ๊ณต ํ์ด์ง
๊ธฐ์กด ์ฝ๋ ๋ฆฌํฉํ ๋ง(์ฝ๋ ์ ๋ฆฌ)