안녕하세요.
저희 아이웨딩에서 스드메 계산기 개편 작업을 하게 되었습니다
기존 스드메 상품 3개를 바로 선택해 견적을 내는 플로우 였지만
개편 작업은 스드메 각각 브랜드를 먼저 선택후 상품을 정하는 방식으로 개편되었습니다.
라우트에 해당되는 파일을 생성한후 서버사이드에서 처리 데이터는 빠른 TTFB를 위해 SEO, 비로그인시 로그인 페이지로 리다이렉트 시키는
로직만 처리했습니다
pages에 이름에 맞는 라우트 파일 생성 및 서버사이드 처리후 클라이언트 사이드 컴포넌트에서
query string(step=1,step=2,step=3)으로 분기처리 했습니다.
클라이언트단에서 데이터 처리 SWR로 처리 하였기 때문에 캐싱되어 step이 바뀌더라도 매번 데이터를 불러오지 않도록 했습니다
const router = useRouter();
const { step = '1' } = router.query;
return (
<Wrapper>
<TitleBox />
<SdmTabs />
{step === '1' && <SdmStep1 />}
{step === '2' && <SdmStep2 />}
{step === '3' && <SdmStep3 />}
</Wrapper>
);
업체 데이터는 SWR 커스텀 훅으로 만들어 요청하고 있습니다
스튜디오/드레스/헤어메이크업을 tab으로 나누어 요청하고 있습니다.
export const useSdmBrandList = () => {
const router = useRouter();
const { tab = '사진' } = router.query;
const result = useSWR(`/ibrandplus/sdmBrandList?tab=${tab}`, fetcher);
return result;
};
const { data } = useSdmBrandList();
//framer-motion
const [scope, animate] = useAnimate();
useEffect(() => {
if (empty(data)) {
animate('li', { opacity: 1 }, { delay: stagger(0.007) });
}
}, [data]);
return (
<Wrapper ref={scope}>
{isArray(data).map(item => {
const isActive = sdmEntCodeArr.includes(item.enterprise_code);
return (
<li className='brand_list' key={`${item?.enterprise_code}_list`}>
<button className={isActive && 'active'} type='button' onClick={() => onBrandClick(item)}>
<p>
{item.bpchk === 1 && (
<span className='best'>
BEST
<br />
</span>
)}
{item.enterprise_name}
</p>
<span>
<화보바로가기아이콘 isActive />
</span>
</button>
</li>
);
})}
</Wrapper>
)
map으로 데이터를 풀기 이전에 Wrapper 최상위 div에 framer-motion stagger 애니메이션 효과를 추가하여
자연스럽고 native하게 처리 하였습니다
========================= step=1 결과물 =========================
step1에서 선택한 업체에 대해서 업체코드::업체명을 query string으로 상태를 관리하여 해당 업체들의 상품들을 가져오도록 하였습니다
s:스튜디오 d:드레스 m:메이크업의 약자이고
"s_ent=co_sl_341::가을스튜디오" 형식으로 상태를 관리하여 추후에 split("::")로 업체코드, 업체명을
파싱하여 사용이 가능합니다
export const useSdmProduct = () => {
const router = useRouter();
const { s_ent, d_ent, m_ent } = router.query;
const result = useSWR(`/product/sdmProductList?entCodes=${entCodes.join('::')}`, fetcher);
return result;
};
UI컴포넌트 디자인은 컴파운드 디자인을 사용하여 중복되는 상품카드 컴포넌트를 처리했습니다
step2 컴포넌트도 보다 native스럽게 만들기위해 framer-motion stagger를 사용하였습니다
const { data } = useSdmProduct();
const [scope, animate] = useAnimate();
useEffect(() => {
if (empty(data)) {
animate('div', { opacity: 1 }, { delay: stagger(0.007) });
}
}, [data]);
return (
<Wrapper ref={scope}>
<SdmProductCard>
<SdmProductCard.Header data={data}/>
<SdmProductCard.List data={data} isTotal={isTotal} />
</SdmProductCard>
</Wrapper>
)
========================== step=2 결과물 ===========================
견적의 마지막 페이지로써 step=2에서 불러온 상품 데이터를 SWR캐싱이 되어 있기때문에 바로 데이터를 가지고 렌더링할 수 있도록 하였습니다
전체적인 컴포넌트 애니메이션은 opacity처리만 해주어 자연스럽게 만들었습니다
캐싱한 데이터를 step=2에서 선택한 상품에 맞는 상품를 찾아 정규화 하여 하위 컴포넌트에 props 보내주었습니다
const { data } = useSdmProduct();
const productList = Object.entries(data).map(([category, products]) => {
return { category, data: products.find(item => sdmProductNoArr.includes(item.no)) };
});
<Wrapper animate={{ opacity: [0, 1] }} initial={{ opacity: 0 }}>
<ProductList productList={productList} />
<ProductPriceBox productList={productList} setMaxPrice={setMaxPrice} />
<AddEventButtonBox />
<AddDiscountBox maxPrice={maxPrice} />
<ReferBox />
</Wrapper>
========================== step=3 결과물 ============================
framer-motion을 이용한 애니메이션은 디자인과 기획이 되어있지 않고 오로지 저의 만족으로 인해 작업이 되었습니다.
그래서 디자인 & 기능 통합테스트 기간동안 애니메이션을 소소하게 추가한것만으로도 큰 만족감을 얻었다는 피드백을 들었습니다.
요즘 웹앱에서는 native하게 만드는것이 중요하다고 생각하기 때문에 적절한 애니메이션 추가는 도움이 크게 되는거 같습니다.
프론트를 개발하면서 step 1 2 3를 밟아가며 하나의 목적을 가진 페이지에서 순차적으로 결과페이지까지 가는 패턴을
토스에서는 '퍼널'이라고 하는것 같습니다
퍼널 패턴을 사용하니 애니메이션도 보다 native하게 적용이 가능한거 같습니다
(스텝별로 애니메이션 추가로 앱처럼 보이게)