파크로그
Published 2022. 9. 20. 18:58
ClickAway Component Project/Co-Studo

ClickAway Component?

Dropdown 이나 Popover 같은 컴포넌트를 만들면, 그 컴포넌트 이외의 영역을 눌렀을 때에도 열려져 있는 상태를 닫게 하고 싶은 경우가 있다.

그것을 위한 구현방법에 대해 이전 프로젝트에서 다른 사람들이 구현한 것을 구경해보았을 때 window 에 click listener 를 달아서 해결한다던지, open 상태에 따라 전체 영역을 감싸는 div 를 만들어 사용하는 방법이라던 지 (이전 프로젝트에서 썼던 방법) 등의 방법이 있었다.

 

ClickAway 라고 멋대로 이름을 짓긴 했는데, 바깥 영역을 눌렀을 때 어떤 동작을 하고싶음 이라는 것은 다양한 곳 (Dropdown, Popover, Modal  등) 에서 쓰일 수 있기 때문에 headless 컨셉의 공통 컴포넌트로 분리하여 사용하자고 이야기했다.

 

컨셉은 이전에 만들었던 Dropdown 에서 사용되고 있는 로직을 분리하는 것 이었는데 고민 사항은 아래와 같았다.

 

1. Dropdown Trigger 를 누르면 Trigger element 의 height 를 계산하여 List 의 layout 를 잡는데, Trigger 의 height 관련한 상태는 어디에 둘 것인가?

2. Open 과 관련된 상태를 ClickAway 에 포함시키는게 맞는가?

 

1 번은 Dropdown Trigger 의 위치에 따른 Dropdown List layout positioning 이므로 Dropdown 에서 다루는 것이 맞다고 판단했다.

2 번은 @mui/base 에 포함되어있는 ClickAwayListener 를 참고하니 onClickAway 라는 prop 으로 핸들러를 전달받아 사용시킨다.

즉 open 과 관련한 상태는 포함되어있지 않은 것인데 그럼 onClickAway 는 ClickAwayListener 로 감싸져 있는 컴포넌트가 사용되는 곳에서는 onClickAway 가 항상 발생하는 것인지 궁금해서 테스트해보았다. 결론은 그렇게 동작했다. 

 

 

원하지 않는 Side Effect 라고 생각해서, 내가 만드는 컴포넌트에서는 Open 상태일 때만 ClickAway 를 감지하도록 하고 싶었다.

그래서 ClickAway 의 Open 상태를 두는 것이 맞다고 판단했다.

 

일단 같이 프로젝트를 진행하는 분들에게 리뷰는 받지 않은 상태이지만, ClickAway 의 코드는 아래와 같다.

 

import {
  createContext,
  Dispatch,
  ReactNode,
  useContext,
  useReducer,
} from 'react';
import { css } from 'styled-components';

type ClickAwayState = {
  isOpen: boolean;
};

type ClickAwayAction = { type: 'TOGGLE_OPEN_AWAY' };

const reducer = (state: ClickAwayState, action: ClickAwayAction) => {
  switch (action.type) {
    case 'TOGGLE_OPEN_AWAY':
      return { isOpen: !state.isOpen };
    default:
      return state;
  }
};

const initState = {
  isOpen: false,
};

const ClickAwayContext = createContext<
  [ClickAwayState, Dispatch<ClickAwayAction>]
>([initState, () => {}]);

export const useClickAwayContext = () => {
  const context = useContext(ClickAwayContext);
  return context;
};

const ClickAway: React.FC<{ children: ReactNode }> = ({ children }) => {
  const clickAwayReducer = useReducer(reducer, initState);
  const [state, dispatch] = clickAwayReducer;

  return (
    <ClickAwayContext.Provider value={clickAwayReducer}>
      <div
        css={css`
          &::before {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            content: '';
            z-index: 90;
            display: ${state.isOpen ? 'block' : 'none'};
          }
        `}
        onClick={() => {
          dispatch({ type: 'TOGGLE_OPEN_AWAY' });
        }}
      >
        {children}
      </div>
    </ClickAwayContext.Provider>
  );
};

export default ClickAway;

 

코드의 변경사항은 PR 에서 확인할 수 있다.

profile

파크로그

@파크park

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!