[21/07/17] 업체 설명 모달 마무리 & 찜하기 디자인

오즈

onClickDropDownItem 함수 분리

  1. 품목 드랍다운 아이템 선택
// 품목 드랍다운 아이템을 선택하면 실행된다.
 const onClickItem = ({ e, value }) => {
    if (value.id === select.item) {
      // 이미 선택한 품목이면 지워줘야함.
      setSelect((prev) => ({
        ...prev,
        item: null,
        kind: null,
        hashtags: null,
      }));
      setCurrentItem('품목');
      setCurrentKind('종류');
      setCurrentHashtags(['해시태그']);
      return;
    }
   // 선택하지 않았던 품목이면 변경.
    setSelect((prev) => ({
      ...prev,
      item: value.id,
      kind: null,
      hashtags: null,
    }));
    setCurrentItem(value.name);
    setCurrentKind('종류');
    setCurrentHashtags(['해시태그']);
  };
  1. 종류 드랍다운 아이템 선택
  const onClickKind = ({ e, value }) => {
    if (value.id === select.kind) {
      // 이미 선택했던 종류이면 초기화
      setSelect((prev) => ({ ...prev, kind: null, hashtags: null }));
      setCurrentKind('종류');
      setCurrentHashtags(['해시태그']);
      return;
    }
    // 선택하지 않았던 종류면 변경해준다.
    setSelect((prev) => ({ ...prev, kind: value.id, hashtags: null }));
    setCurrentKind(value.name);
    setCurrentHashtags(['해시태그']);
  };
  1. 해시태그 드랍다운 아이템 선택
  const onClickHashtags = ({ e, value }) => {
    if (select.hashtags && select.hashtags.includes(value.id)) {
      // 이미 선택한 해시태그의 경우 변경해준다.
      setSelect((prev) => ({
        ...prev,
        hashtags: prev.hashtags.filter((hashtag) => hashtag !== value.id),
      }));
      setCurrentHashtags((prev) =>
        prev.filter((hashtag) => hashtag !== value.name),
      );
      return;
    }
    // 처음 선택하는 해시태그의 경우 변경
    setSelect((prev) => ({
      ...prev,
      hashtags: prev.hashtags ? [...prev.hashtags, value.id] : [value.id],
    }));
    setCurrentHashtags((prev) =>
      prev[0] !== '해시태그' ? [...prev, value.name] : [value.name],
    );
  };
  1. 제작 가능 수량 드랍다운 아이템 선택
 const onClickQuantity = ({ e, value }) => {
    setSelect((prev) => ({
      ...prev,
      quantity: value === '직접입력' ? '0' : value,
    }));
    setCurrentQuantity(value);
  };

하나의 함수를 네개로분리. 함수는 하나의 기능만을 수행하도록 구성하는 것이 좋다. 하지만 리팩토링이 필요해보임.

클릭했던 DropDownItem 다시 누르면 초기화

select의 값과 같다면 이미 선택된 것이므로 초기화해주는 로직

if (value.id === select.item) {
  // 지금 선택한 value의 아이디가 select.item의 값과 같다면
  // 이미 선택했던 품목이다.
      setSelect((prev) => ({
        ...prev,
        item: null,
        kind: null,
        hashtags: null,
      }));
      setCurrentItem('품목');
      setCurrentKind('종류');
      setCurrentHashtags(['해시태그']);
      return;
    }
// 이미 선택했던 해시태그인지 점검하려면 밑과 같이 하면됨. value.id가 select.hashtags에 포함되어있다면 이미 선택된 해시태그인것임
if (select.hashtags && select.hashtags.includes(value.id)) {
      setSelect((prev) => ({
        ...prev,
        hashtags: prev.hashtags.filter((hashtag) => hashtag !== value.id),
      }));
  // `['123'].join(",")` => `'123'`
      setCurrentHashtags((prev) =>
        prev.filter((hashtag) => hashtag !== value.name),
      );
      return;
    }

currentHashtags 상태는

위 사진 텍스트들을 출력해주기 위해 선택된 해시태그들의 이름을 담고있다. 처음에는 문자열로 저장하여 ${prev},${newStiring}과 같이 상태값을 저장하였는데, 선택한 해시태그를 지울때 문제가 생길 수 있다는 점을 알게되었다.

문자열로 저장하는 경우, 해시태그12 해시태그1 를 선택하여 저장했다고 가정하자, 이 경우 해시태그1를 지우려면 string.replace('해시태그1','') 해주어야하는데 상관없는 해시태그122가 되어버려 이슈가 발생할 수 있다.

이러한 문제점을 해결하기위해 currentHashtags 상태를 배열값으로 저장하고 사진에서 처럼 보여주어야하는 경우 array.join(",")을 사용하여 보여주기로함.
다음과 같이 저장하여

문자열로 보여줘야하는 경우 join하여 다음과 같이 만들어 보여준다

리스트가 없으면 보여주는 컴포넌트 만들기


위 컴포넌트를 만들어주어야하는데 고려사항이 몇가지 있다.
우선 해시태그의 경우 품목이 비어있으면 품목 선택을 띄워줘야하고, 종류가 비어있으면, 종류 선택을 띄워줘야한다.

그렇기 때문에 select(품목, 종류, 해시태그, 제작가능수량 타입들이 어떤 값으로 저장되어있는지 관리하는 상태값)을 인자로 넘겨주어 select.item이 있는지 없는지를 점검하여 각각 맞는 None 컴포넌트를 띄워주도록한다.

위와 같이 구현하였다. 여기서 키포인트는 이벤트 버블링이다. 처음엔 Button을 눌러 드랍다운을 꺼야겠다는 생각을 하고 개발을 하였다.

생각해보니 이벤트 버블링이 되어 굳이 드랍다운 끄는 로직을 달지 않아도

 const onClickFilterItem = () => {
    //   // event 버블링에 의해 누르면 꺼지게됨.
    //   setIsActive(!isActive);
    setFocus((prev) => {
      Object.keys(prev).forEach((key) => {
        if (key !== type) prev[key] = false;
      });
      return {
        ...prev,
        [type]: !prev[type],
      };
    });
  };

...

 <Wrapper focus={focus[type]} onClick={onClickFilterItem}>
          {current}
          {focus[type] && (
            <DropDownList>
              {list ? (
                list.map((l) => (
                  <DropDownItem
                    key={l.name}
                    onClick={(e) => onClick({ e, value: l })}
                  >
                    {l.name}
                  </DropDownItem>
                ))
              ) : (
                <None select={select}></None>
              )}
            </DropDownList>
          )}
          <Icon icon="Arrow" size={10} />
        </Wrapper>

Wrapper의 자식을 클릭하면 onClickFilterItem이 실행되어 드랍다운이 꺼지게된다.

중복선택이 가능한 해시태그 드랍다운

해시태그는 중복선택이 가능하다 그렇기 때문에 아이템을 선택하더라도 드랍다운이 꺼지는 일이 생겨선 안된다.

지금 코드로는 이벤트 버블링에의해 DropDown에서 클릭이벤트가 발생하면 부모의 클릭 이벤트로 퍼져 드랍다운이 꺼지게된다. 이를 해시태그인 경우에는 막으려고한다.

const onClickHashtags = ({ e, value }) => {
    e.stopPropagation(); // 이벤트가 부모 이벤트로 퍼지는 것을 방지한다.
   	....
  };

자식 아이템을 클릭하여 클릭이벤트를 실행하여도 부모 이벤트가 실행되는 일은 생기지 않는다.

자식에서 뻗어나가는 이벤트를 막으려면 자식에 e.stopPropagation() 코드를 추가하자.

이벤트 버블링

하위의 클릭이벤트가 상위로 전달되어가는 그림.

즉 one > two > three의 DOM구조에서 맨 마지막 자식 three를 클릭하면, 이벤트가 다음과 같이 실행된다.

이벤트 캡처

이벤트 버블링이 자식에서 부모 순서로 이벤트가 전달된다면, 이벤트 캡처는 부모에서 자식으로

<body>
	<div class="one">
		<div class="two">
			<div class="three">
			</div>
		</div>
	</div>
</body>
....

var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
	div.addEventListener('click', logEvent, {
		capture: true // default 값은 false입니다.
	});
});

function logEvent(event) {
	console.log(event.currentTarget.className);
}

code라면 이벤트는 one -> two -> three 순서로 진행된다. 이벤트 버블링과 반대로 진행. addEventListenercapture:true옵션을 주면 이벤트 캡처 가능하다.

e.stopPropagation()

이벤트 버블링과 캡처를 막는 코드.
이벤트 버블링의 경우 상위요소로 이벤트가 전달되는 것을 막는다. 즉 누른 자기 자신의 이벤트 핸들링 메소드만 실행된다.
이벤트 캡처의 경우 최상위 요소만 이벤트 핸들링 메소드가 실행.

이벤트 위임

하위 요소에 각각 이벤트를 붙이지 않고 상위 요소에서 하위 요소의 이벤트들을 제어하는 방식

인풋들에 click이벤트를 달고자 한다. 그렇다면 다음과 같은 코드로 작성될 것이다.

const inputs = document.querySelectorAll('input')
inputs.forEach(input=>input.addEventListener('click',handler)

위 처럼 각각 요소에 이벤트를 다는 방식이 문제가되는 이유


1. 나중에 추가되는 인풋들에는 클릭이벤트가 달리지 않게된다.
2. 요소가 수백 수천개라면, 같은 기능을 하는 이벤트를 수백 수천개 달아주어야함(비효율)

따라서 이벤트 위임을 이용하여 상위요소에 클릭이벤트를 달아 하위요소를 제어하자

// .itemList는 input들을 감싸고 있는 상위 요소
var itemList = document.querySelector('.itemList');
itemList.addEventListener('click', function(event) {
	alert('clicked');
});

선택된 카테고리 정보를 가지고 업체탐색페이지 라우팅

  const onClickSearchButton = () => {
    router.push({
      pathname: '/explore/result',
      query: { ...select },
    });
  };

select 에는 쿼리 property 별 값들이 저장되어있는 객체임.

백그라운드에 돋보기 이미지 삽입하기


컴포넌트안에 다음 style을 추가한다.

  background-image: url('/image/find.png');
  background-repeat: no-repeat;
  background-position: right bottom;
// background-position => x는 오른쪽 y는 아래 위치하게.

품목 바 만들기

클릭 처리를 Link or onClick 중 어떤 것으로?

현재 품목은 18개이지만, 실제로 품목 결과 화면으로 이동이 가능한 품목은 5개이다. 그렇기에 품목 결과 화면이 없는 품목의 경우 이동이 불가능하게 해야하는데, Link 컴포넌트로 이 처리를 하기엔 코드의 가독성이 떨어져 복잡해질 우려가 있다.

처음엔 다음과 같이 Link 컴포넌트로 구현을 하였는데, 생각해보니 품목 결과 화면이 없는 품목의 경우도 이동이 가능하다는 걸 알게되었다. item.idcategory(실제 품목 결과 화면이 있는 품목들)에 포함되어있는지를 체크하여 Link로 달아주는 것을 생각함.

{itemList.current.map((item) => (
  // category.includes(item.id)가 true/false인지 점검하여 Link로 감쌀지 안감쌀지를 정한다
  // 코드 길어짐.
                <Link
                  key={item.id}
                  href={{
                    query: { item: item.id },
                    pathname: '/explore/result',
                  }}
                >
                  <ItemWrapper
                    key={item.id}
                    include={category && category.some((c) => c.id === item.id)}
          
                  >
                    {<Icon icon={`Icon${item.id}`} size={32} />}
                    <ItemText>{item.text}</ItemText>
                  </ItemWrapper>
                </Link>
              ))}

코드 가독성이 떨어질 것을 우려하여 다음과 같이 onClick 핸들러에서 처리해주도록 구현하였다. dataset을 이용하여 클릭 시 element에서 item.id값을 조회할 수 있도록 구현함.

{itemList.current.map((item) => (
                <ItemWrapper
                  key={item.id}
                  // 만약 준비된품목이면 opacity를 1로 둔다.
                  // 아니면 0.4
                  include={category && category.some((c) => c.id === item.id)}
                  
                  data-id={item.id}
                  // 클릭하면 이동하는 로직
                  onClick={onClickItem}
                >
                  {<Icon icon={`Icon${item.id}`} size={32} />}
                  <ItemText>{item.text}</ItemText>
                </ItemWrapper>
))}
const onClickItem = ({
    currentTarget: {
      dataset: { id },
    },
  }) => {
  // 준비된 상품이면 라우팅
    if (category.some((c) => c.id === +id)) {
      router.push({
        pathname: '/explore/result',
        query: { item: id },
      });
      return;
    }
  // 준비되지 않았으므로 alert 띄워주기
    alert('준비 중인 품목입니다.');
  };

해야할일

  • 업체 결과 페이지 스켈레톤 UI 적용하기
  • 데스크탑일때 스타일 맞추기
  • 찜하기 시작하기
  • 리팩토링

Ref

좋은 웹페이지 즐겨찾기