공통 컴포넌트를 미리 만들어놓는 것은, 통일성 있는 UI를 제공하기 위해 필수적인 부분이다.
공통 컴포넌트의 예시들로, 모달, 버튼, 입력 박스 등이 있겠지만, 그 중 가장 자주 쓰이는 Modal에 대해 알아보고자 한다.
전체 코드는 아래에 있지만, 내가 몰랐던 부분을 하나하나씩 알아보며 차근차근 같이 공부해볼 수 있도록 하자. 사실 이 게시물을 프로젝트를 하고 2달 반 정도 뒤에 작성하고 있는데, 공부하다보니 계속해서 새로운게 나온다.
import { ReactNode, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import * as styles from './index.css';
useRef : Dom 요소에 직접 접근할 수 있게 해주는 훅.
createPortal : React에서 포탈을 생성하여 컴포넌트 트리 외부의 DOM 노드로 자식을 렌더링할 수 있게 함.
const Modal = ({ children, isOpen, onClose }: Props) => {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
if (isOpen) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [isOpen]);
return createPortal(
<dialog ref={ref} className={styles.modal} onClose={onClose}>
<div className={styles.content}>{children}</div>
</dialog>,
document.getElementById('modal') as HTMLDivElement,
);
};
ref 를 정의 하는 부분
: useRef 훅을 사용하여 'dialog' 요소에 대한 참조를 생성한다. 이 Type은 'HTMLDialogElement' 인데, html에서 사용되는 dialog 태그에 대한 Type이다.
useEffect 훅을 사용 - 컴포넌트가 렌더링 후에 효과를 수행하게 해주는 useEffect 훅을 사용하여 isOpen 값이 변경될 때마다 실행되게 한다.
createPortal 사용
1) createPortal을 사용하여 모달을 특정 DOM 노드 ('modal' ID를 가진 요소) 로 랜더링함.
2) 'dialog' 태그는 'ref'로 참조됨.
3) {children} 은 'div' 요소 내부에 렌더링된다.
이렇게 만들어진 Modal 컴포넌트는 useModal Hook을 통해 사용되어진다.
// useModal Hook의 html 부분
const Modal = ({ children }: Props) => {
return (
<ModalComponent isOpen={isOpen} onClose={closeModal}>
{children}
</ModalComponent>
);
};
이에서 'ModalComponent' 는 우리가 위에서 만든 const Modal을 선언함으로 연결된다. Modal: ModalComponent를 래핑하는 컴포넌트.
return { Modal, openModal, closeModal, isOpen };
};
export default useModal;
이렇게 선언된 Hook은 아래와 같은 Return 값을 통해 사용될 수 있다.
아래는 사용한 예시이다.
const { Modal, openModal, closeModal } = useModal();
<ColoredButton
size='xsmall'
text='바로가입'
color='yellow'
onClick={openModal}
/>
<Modal>
<SignUp closeModal={closeModal} />
</Modal>
회원가입에서 위와 같은 예시를 통해 사용이 가능하다.
아래는 전체 구문.
//modal.tsx
import { ReactNode, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import * as styles from './index.css';
interface Props {
children: ReactNode;
isOpen: boolean;
onClose: () => void;
}
const Modal = ({ children, isOpen, onClose }: Props) => {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
if (isOpen) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [isOpen]);
return createPortal(
<dialog ref={ref} className={styles.modal} onClose={onClose}>
<div className={styles.content}>{children}</div>
</dialog>,
document.getElementById('modal') as HTMLDivElement,
);
};
export default Modal;
//useModal.tsx
import { ReactNode, useState } from 'react';
import ModalComponent from '@/components/Modal/index';
interface Props {
children: ReactNode;
}
const useModal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
const Modal = ({ children }: Props) => {
return (
<ModalComponent isOpen={isOpen} onClose={closeModal}>
{children}
</ModalComponent>
);
};
return { Modal, openModal, closeModal, isOpen };
};
export default useModal;