최근 토스 SLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트 컨퍼런스를 통해 전역상태를 이용해 모달이 띄워지는 Select 컴포넌트 인터페이스를 설계하는 법에 대해 배우게 되었다.
영상의 댓글에서 해당 컴포넌트에 대한 자세한 설명이 담긴 블로그 포스팅을 공유받게 되었다.
원티드 프리온보딩 프론트엔드 부트캠프에서 모달이 띄워지는 게시물을 만들어야 했는데, 위의 포스팅을 열심히 참고해 나름대로 구현에 성공했다!
아래 코드 샌드박스는 내가 만든 모달을 띄운 게시물이다. 게시물(숫자 1,2,3) 클릭하면 게시물의 content가 모달에 띄워지는 것을 확인할 수 있다!
해당 포스팅은 모달을 띄우는 게시물을 만드는데 생각했던 고민들과 결과물에 대한 자세한 설명을 다루어보고자 한다.
Card가 가져야할 기능을 중심으로 정리해본 요구사항들은 다음과 같다.
앞서 정의한 요구사항은 어떤 방법으로, 어떤 상태관리를 통해 구현할 수 있을까?
위에서 정의한 요구사항을 토대로 구현방향을 자세하게 설명해보자.
isOpen
상태 필요하다.close 버튼
과 이후 작업을 수행할 submit 버튼
이 있어야 한다.앞서 구현 설명 때 Modal에서 필요한 상태를 Context API로 관리하기로 말했다.
Modal의 상태는 크게 4가지이다.
isOpen
: 모달의 렌더링 여부 결정openModal
: 모달을 여는 콜백 함수closeModal
: 모달을 닫는 콜백 함수modalData
: 모달 content, 모달에서 수행할 처리 로직들 등등...우리는 Card를 클릭 시 Modal에 렌더링할 content와, 열고 닫을 때 처리 로직을 Modal에 주입을 시킬 것이다.
children
: 모달 contentonCancel
: 모달을 닫을 때 처리할 로직onSubmit
: 모달을 submit할 때 처리할 로직그럼 아래의 그림 형태로 구조가 잡히게 된다.
이제 앞서 설명한 구조를 코드로 하나하나 설명하고자 한다.
불필요한 인터페이스를 제거하고 Modal의 state를 숨김으로써 추상화 레벨을 높이는데 집중해보자.
상태를 숨기기 위해 Context API를 이용하자.
type ModalData = {
children?: ReactNode;
onCancel?: () => unknown;
onSubmit?: () => unknown;
};
const ModalContext = createContext<{
isOpen: boolean;
openModal: (modalData: ModalData) => unknown;
closeModal: () => unknown;
modalData: ModalData;
}>({} as any);
export const ModalContextProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [modalData, setModalData] = useState<ModalData>({});
const openModal = ({ children, onCancel, onSubmit }: ModalData) => {
setIsOpen(true);
setModalData({
children,
onCancel,
onSubmit,
});
};
const closeModal = () => {
setIsOpen(false);
setModalData({});
};
return (
<ModalContext.Provider value={{ isOpen, openModal, closeModal, modalData }}>
{children}
</ModalContext.Provider>
);
};
Modal context에서는 위의 그림처럼 children, onCancle, onSubmit을 인자로 전달받는다.
openModal
closeModal
export const Modal = () => {
const { isOpen, modalData, closeModal } = useModalContext();
if (!isOpen) {
return <></>;
}
const { children, onCancel, onSubmit } = modalData;
const onCancelInternal = () => {
onCancel?.();
closeModal();
};
const onSubmitInternal = () => {
onSubmit?.();
closeModal();
};
return (
<Portal>
<div className="modal">
<div className="modal__dropdown" onClick={closeModal} />
<div className="modal__contents">
<div>{children}</div>
<div className="modal__actions">
<button onClick={onCancelInternal}>cancel</button>
<button onClick={onSubmitInternal}>submit</button>
</div>
</div>
</div>
</Portal>
);
};
Modal Context api에서 정의한 비즈니스 로직을 이용해 모달 UI를 만들어보자.
모달은 isOpen
상태에 따라 렌더링 여부를 결정한다.
onCancelInternal
onSubmitInternal
import { memo } from "react";
import { Modal, useModalContext } from "./components/Modal";
const CardList = memo(() => {
const { openModal } = useModalContext();
const openModalWithData = (data: number) =>
openModal({
children: <p>{data}</p>,
onSubmit: () => console.log("submit")
});
return (
<>
{[1, 2, 3].map((data) => (
<p key={data} onClick={() => openModalWithData(data)}>
{data}
</p>
))}
</>
);
});
export default function App() {
return (
<div className="App">
<CardList />
<Modal />
</div>
);
}
CardList는 게시물 list를 렌더링한다.
각 게시물을 클릭 시 모달을 렌더링해야 하므로 context api에서 정의한 openModal 콜백함수를 이용한다.
openModalWithData
각 게시물에 openModalWithData 콜백함수를 이벤트 핸들러로 걸어준다.
토스 컨퍼런스를 보던 당시 도저히 이해를 못하겠던 코드를 1달 사이에 조금이라도 이해해 적용을 하게되어서 정말 기쁘다.
하루하루 공부하면서 내가 잘하고 있는 것이 맞을까 하고 두려울 때가 많았으나, 이번 구현을 통해 "아! 그래도 성장하고 있긴하구나!" 라는 생각이 들었다.
늘 조금씩 차근차근 착실히 성장해야겠다!
토스 컨퍼런스를 보고 구현하려다가 어려움을 겪으신 분들, 또는 모달을 구현하시는 분들에게 이 글이 조금이라도 도움이 되길 바란다.
(ref)