본문 바로가기

dev/React

redux를 이용해 react modal을 효율적으로 관리하는 방법

이전에 봤던 코드 중에 전체 프로젝트의 모달을 하나의 컴포넌트에서 제어하는 것을 보았습니다.

이번에 모달을 만들 일이 생겨 관련 내용을 검색해보니 실제로 저와 같은 생각을 하시는 분도 종종 계셨습니다. [Okky 질문글 링크]

 

링크를 따라가 보시면 댓글에서 Practical Redux, Part 10: Managing Modals and Context Menus 라는 글을 소개하고 있는데, 조금 오래된 글이지만 이 글을 읽고 대략적인 구조를 잡을 수 있었습니다.

 

본론에 앞서, 이 작업의 목표를 정의하자면 '모달의 상태, 렌더링을 다른 컴포넌트에서 분리하자' 입니다.

자세히 말하자면 다른 컴포넌트에 isOpen 같은 state를 두지 말고 redux 스토어로 관리하는 것이 첫 번째 목표입니다. 그리고 렌더링도 모달 ui를 위한 하나의 컴포넌트에서 수행하자는 것이 두 번째 목표입니다.

 

1. 구조

그림... 을 그린다는게 참 쉽지 않은 것 같습니다...🙄

간단하게 설명드리면 우선 modalIndex라는 파일을 생성합니다. 그리고 여기에 키-값 형태로 모달 컴포넌트의 정보를 미리 저장해 둡니다. 예를 들면 A모달의 컴포넌트 path 같은 것을 저장합니다. 그 후, 리덕스 store에 modal을 추가해 줍니다. 마지막으로 모달을 렌더링할 ModalManager에서 store.modal 값을 사용하도록 합니다. store.modal값이 변하면 ModalManager는 이 값을 key로 하여, modalIndex에서 component 정보를 가지고 렌더링합니다. 

 

2. modalIndex 생성

ModalManager가 컴포넌트에 대한 정보를 가져올 수 있는 lookup table을 생성합니다. 저는 store에는 key 값만 저장하고 다른 정보는 modalIndex에서 찾을 것이기 때문입니다. 글을 쓰면서 자료를 더 찾아 봤는데 이 방법 말고도 store에 아예 컴포넌트 자체를 저장하는 방법도 있는 듯 합니다. 

const modalIndex: TModalState[] = [
  {
    type: "SampleModal",
    component: loadable(() => import("./SampleModal"))
  },
  {
    type: "SampleModal222",
    component: loadable(() => import("./SampleModal222"))
  },
  ...
];

3. redux store에 modal 추가하기

모달을 위한 액션, 리듀서를 스토어에 추가해 줍니다. 저는 redux-toolkit을 사용해 작성했습니다.

const modalSlice = createSlice({
  name: "modal",

  initialState: {
    opened: [] as TModalState[]
  },

  reducers: {
    open(state, action: PayloadAction<TModalState>) {
      const modalConfig = action.payload;
      const { type } = modalConfig;
      !state.opened.find(mt => mt.type === type) &&
        state.opened.push(modalConfig);
    },
    
    ...

4. useModal hook 작성하기 (optional)

편리한 사용을 위해 hook을 작성했습니다. 아직은 간단한 기능 뿐이라서 hook도 간단합니다!

export default function useModal() {
  const dispatch = useDispatch();
  const openModal = (payload: TModalState) => {
    dispatch(open(payload));
  };
  const closeModal = () => {
  	...
  };
  return { openModal, closeModal };
}

5. ModalManager 작성하기

export default function ModalManager() {
  const openedModals = useSelector((state: RootState) => state.modal.opened);

  useEffect(() => { 
    Modal.setAppElement("#__next");
  }, []);

  return (
    <Container>
      {openedModals.map((modal, idx) => {
        // index 에 지정된 component 정보 가져오기
        const preConfig = modalIndex.find(mi => mi.type === modal.type);
        if (!preConfig || !preConfig.component) {
          console.error("can't find modal component");
          return null;
        }
        const ModalComponent = preConfig.component;

        const { props, options } = modal;

        return (
          <Modal
            key={idx}
            isOpen={true}
            {...(options && lodash.omitBy(options, !lodash.isUndefined))}
          >
            <ModalComponent {...props} />
          </Modal>
        );
      })}
    </Container>
  );
}

먼저 useSelector를 통해 store에서 modal 정보를 받아옵니다(openedModals). 그 후, type 이라는 키로 인덱스에서 모달에 대한 정보를 가져왔습니다(preConfig). 마지막으로 모달 정보 중 컴포넌트를 랜더링합니다. 

 

위 코드에는 본문에는 없던 props와 option을 전달하고 있습니다. 저는 modal payload에 props와 option도 함께 전달하여 모달 생성 시 사용하였습니다. 다만 흐름에 있어 핵심적인 내용은 아닌지라, 본문에서는 제외했습니다. 

 

덧붙여 useEffect의 Modal.setAppElement()는 react-modal 이라는 모듈을 사용할 때 호출해야 하는 함수입니다. 직접 모달 컴포넌트를 구현하신다면 신경쓰지 않으셔도 됩니다.

 

마지막으로 App에서 ModalManager를 호출하게 해 주면 됩니다.

function MyApp({ Component, ...pageProps }: AppProps) {
  const { store, props } = wrapper.useWrappedStore(pageProps);
  return (
    <Provider store={store}>
      <GlobalStyle />
      <Component {...props.pageProps} />
      <ModalManager />
    </Provider>
  );
}

6. 마치며

원래는 마무리 글을 안쓰는데 본 포스팅은 기존 글과 달리 뇌피셜...이 많습니다. 그냥 아이디어를 구현하는 차원에서 러프하게 제작한 것이니, 글을 읽으실 때 참조 부탁드립니다. 또한 문제점이나 개선점이 있다면 댓글 부탁드리겠습니다! 저도 이 모달을 좀 더 만져보다 발견하는 것이 있다면 포스트를 업데이트 하도록 하겠습니다! 혹시나.. 저의 뇌피셜 때문에 삽질하는 분이 없으시길 바라며 글 마치겠습니다. 감사합니다.