이전에 봤던 코드 중에 전체 프로젝트의 모달을 하나의 컴포넌트에서 제어하는 것을 보았습니다.
이번에 모달을 만들 일이 생겨 관련 내용을 검색해보니 실제로 저와 같은 생각을 하시는 분도 종종 계셨습니다. [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. 마치며
원래는 마무리 글을 안쓰는데 본 포스팅은 기존 글과 달리 뇌피셜...이 많습니다. 그냥 아이디어를 구현하는 차원에서 러프하게 제작한 것이니, 글을 읽으실 때 참조 부탁드립니다. 또한 문제점이나 개선점이 있다면 댓글 부탁드리겠습니다! 저도 이 모달을 좀 더 만져보다 발견하는 것이 있다면 포스트를 업데이트 하도록 하겠습니다! 혹시나.. 저의 뇌피셜 때문에 삽질하는 분이 없으시길 바라며 글 마치겠습니다. 감사합니다.
'dev > React' 카테고리의 다른 글
react query optimistic update (0) | 2022.10.23 |
---|---|
react 자식 컴포넌트의 함수를 부모 컴포넌트에 내보내기 (0) | 2022.08.18 |
Next.js TypeScript 환경에서 Jest 초기 설정 (0) | 2021.12.08 |
Redux 개념 이해하기 (0) | 2021.03.08 |
[번역글] react functional component에서 setState를 동기식으로 사용하기 (1) | 2021.03.08 |