왜 내가 숨쉬듯 쓰고 있던 기능들도 내가 구현하려고 하면 어려운걸까? 그리고 난 왜 그걸 만들게 되는걸까? 누가 먼저 만들어놔줬으면 얼마나 좋았을까?! 싶지만..내가 개발자니까 결국엔 내가 또 삽질해가면서 만드는 수 말곤 없지 뭐. 그래서 오늘의 기록은 조합형 옵션 만들기이다!
만약 내가 옷을 사는데 옵션이 한 단계만 가능하다고 하자. 사이즈가 3가지, 색상이 3가지인 옷이라면 옵션이 총 몇 개가 생길까? 바로바로..9개이다. 9개까진 괜찮다고?? 그럼 이거보다 많다면??? 그럼 아래와 같은 일이 생긴다.
이러면 사용자가 옵션을 고르기 힘들어진다. 그래서 대부분의 쇼핑몰들은 조합형 옵션을 제공한다.
하지만 안타깝게도 우리 서비스는 조합형 옵션을 제공하지 않는 쪽이었다. 그래서 내가 이걸 조합형을 사용할 수 있게 바꾸는 작업을 진행했다.
서버에서 클레이풀이라는 서비스를 쓰고 있는데 제품을 가져오는 api를 가지고 내가 프론트에서 후가공해서 썼다. 내가 필요한건 options와 variants이고 각각에 대한 설명을 해보자면 options가 각 조합의 단계 (ex. 색상, 사이즈) variants는 모든 옵션의 조합이다.(검정-s, 검정-m,검정-l,베이지-s...)
이전 코드는 옵션 단계가 하나만 존재한다를 전제해서 짜여있는 코드였다! 즉, 그냥 variants를 하나씩 그냥 보여주는 구조였던 것이다. 이걸 이제 options를 나열하고 선택된 option을 가지고 맞는 variant를 가져오는 구조로 바꾸면 조합형 옵션이 된다. 말만 들으면 어렵지 않지만 구현하는건 차아암 쉽지 않았다.
일단, 지그재그의 조합형 옵션이 어떻게 동작하는지를 참고(+ 디자이너분과의 회의)해서 움직임에 대한 큰 그림을 그렸다.
내가 생각한 플로우는 아래와 같다.
그리고 세부적으로는
1. 만약 검정색(색상-1단계) 전사이즈(사이즈-2단계) 품절인 경우 검정(1단계)에 바로 품절을 노출시킨다.
2. 단계를 선택할 때마다 각 단계에 내가 선택한 옵션명을 보여준다.
요런 로직을 기반으로 조합형 옵션을 만들었다!
나는 위에 정의한 것들을 한번에 다 고려해가면서 만들기엔 같아서 단계별로 나눠서 하나씩 만들었다.
조합형처럼 보이게 단계를 나열해서 보여주기 및 스타일링 적용
이때는 따로 움직임이 있는게 아니라 그냥 모든 단계를 펼쳐서 보여주는 것만 만들었다. 그리고 그 상태에서 피그마에 맞춰 스타일링을 적용했다.
활성화된 단계에서 옵션을 누르면 자동으로 다음 단계로 넘어갈 수 있게하기
쭉 보여주는건 됐으니 이제 활성, 비활성 모드를 만들었다. activateOptionIndex
라는 state를 만들어서 단계별 index가 state 일치하면 활성 아니면 비활성을 진행했다. 그리고 단계에 있는 옵션을 선택하면 activateOptionIndex를 +1 해주며 다음 단계로 넘겼다.
OptionList.jsx
const handleClickOption = (index, variation) => {
const optionLength = product.options.length;
const activeIndex = index + 1;
setActivateOptionIndex(activeIndex);
};
OptionSelector.jsx
useEffect(() => {
setIsActivate(optionIndex === activateIndex);
}, [activateIndex]);
마지막 단계에서 선택하면 모든 옵션이 닫히고 선택 결과 보여주기
마지막 단계에선 activateOptionIndex를 -1로 설정해서 모든 옵션을 비활성화해줬다.
selectedIds
: 각 단계별로 선택된 옵션의 id들
selectedNames
: 긱 단계별로 선택된 옵션명들 - 해당 state로 각 단계 라벨에 선택한 옵션명 노출 및 마지막에 결과는 selectedNamws.join(' / ')
을 통해 보여줌
const handleClickOption = (index, variation) => {
const optionLength = product.options.length;
const activeIndex = index + 1 > optionLength - 1 ? DISABLE : index + 1;
setActivateOptionIndex(activeIndex);
/** 마지막 단계까지 다 선택 완료된 상태인지 확인하고
이미 모든 단계가 선택 완료된 이후이면 다시 1단계로
**/
setSelectedIds(
optionLength === selectedIds.length
? [variation._id]
: [...selectedIds, variation._id]
);
setSelectedNames(
optionLength === selectedIds.length
? [variation.value]
: [...selectedNames, variation.value]
);
};
마지막 단계에서 가격 및 품절 상태노출
const getCurrentAvaliableVariant = useCallback(
(current) => {
const avaliables = variants.filter((item) => {
return selectedIds.every((id) => item.ids.includes(id));
});
return avaliables.find((item) => item.ids.includes(current._id));
},
[selectedIds]
);
const checkIsSoldout = useCallback(
(isVisible, variation, currentAvaliableVariants) => {
// 지금까지 선택된 option id를 모두 포함하는 variants를 찾음
const avaliables = variants
.filter((item) => {
return selectedIds.every((id) => item.ids.includes(id));
})
// 현 단계에서 보여지는 옵션에 대한 variant를 찾음
.filter((item) => item.ids.includes(variation._id));
// 품절된 variant 찾음
const soldoutItems = avaliables.filter((item) => item.stock === 0);
/**
* ex. 검정색 전사이즈 품절(검정색에 품절 표시 필요) or 베이지색 선택 후 s 사이즈가 품절일 때(s사이즈에 품절표시 필요) 와 같은 상황일 때 품절 표시
**/
return (
(isVisible && currentAvaliableVariants.stock === 0) ||
avaliables.length === soldoutItems.length
);
},
[selectedIds]
);
checkIsSoldout
의 결과가 true이면 품절을 false이면 가격을 표시해준다.
옵션 선택 순서 강제하기
내가 만든 방식으로는 1단계 선택 후 3단계를 선택하거나 3단계 선택하다가 1단계로 돌아가려고 하거나 하면 로직이 꼬이게 된다. 저런 모든걸 고려하기엔 힘들어서 우리는 현재 단계를 선택하지 않으면 다음 단계로 넘어갈 수 없게 했다.
const handleOnClickLabel = () => {
// 모든 단계 선택이 끝나면 어느 단계 selector를 누르던 1단계 활성화
if (activateIndex === DISABLE) {
return handleClickLabel(START);
}
// 비활성화된 selector를 누르면 직전 단계일 때 활성화되고 아니면 막음
if (!isActivate) {
return selectedIds.length - 1 === optionIndex
? handleClickLabel(optionIndex)
: confirmPopup({
msg: "현재 옵션을 먼저 선택해주세요.",
buttonType: "sub",
});
}
};
이렇게 단계별로 로직을 확장시켜가면서 아래같은 결과물을 만들어냈다!!!!
처음엔 사실 만들 생각을 하니까 정신도 아득하고..이걸 어떻게 구현해내지??? 싶었다. 하지만 단계별로 생각을 하고 기능을 점진적으로 추가시켜나가니 해결되었다. 그동안 인터넷 쇼핑을 하면서 당연했던 기능이라 그 소중함을 몰랐지만 이젠 조금..소중하게 생각해야겠다 허허.
-끝-