대부분의 프로젝트에서 사용할 수 밖에 없는 모달… 잘 알고있지 않으면 생각보다 구현하기 힘들 수 있다.
지금부터 모달을 만드는 방법을 차근차근 단계별로 알아보자.
💡STEP1
모달 컴포넌트 만들기
우선 간단한 모달 컴포넌트를 만들어보자
export default function Modal(){ return ( <dialog open> <h2>This is Modal</h2> <form method="dialog"> <p>Modal content goes here.</p> <button>Close</button> </form> </dialog> ); }
기본적인 모달 컴포넌트이다.
HTML tag
dialog
를 이용해서 간편하게 구현할 수 있다. 이 dialog
안에 form
method를 dialog로 설정하면 그 안에 버튼을 클릭하게 되면 모달이 닫히게 되는 기본구조가 완성이 된다. 또한 backdrop 즉 뒤에 화면이 어두워지는 효과도 자동으로 지원해주기 때문에 모달을 구현할 때 사용하면 상당이 유용하다.추가로 알아야 하는 사항은
dialog
는 기본적으로 보이지 않는 요소이다. 그래서 강제로 보이게 하기 위해서는 open
prop을 설정해주어야 한다. 하지만 이렇게 강제로 보이게 만들면 뒤에 backdrop이 사라지게 된다. 💡STEP2
backdrop을 살리면서 클릭이벤트를 통해 모달을 open해보자
export default function Parent(){ const dialog = useRef(); const handleClick = () => { dialog.current.showModal() } return ( <div> <Modal ref={dialog}/> {/* 나머지 요소들 */} </div> ); }
export default function Modal({ref}){ return ( <dialog ref={ref}> <h2>This is Modal</h2> <form method="dialog"> <p>Modal content goes here.</p> <button>Close</button> </form> </dialog> ); }
이런식으로 click이벤트가 발생했을 때
dialog
가 지원하는 showModal
함수를 이용하면 open
사용없이 클릭시 모달을 보이게 만들 수 있다??? 사실은 안된다.
💡STEP3
forwardRef를 이용해서 다른 컴포넌트에 ref 전달하기
React에서는 컴포넌트가 전달받은
ref
를 하위 컴포넌트에 전달하려면 forwardRef
를 사용해야 한다. forwardRef
는 부모 컴포넌트에서 생성한 ref를 자식 컴포넌트로 전달할 수 있도록 도와준다. 이를 사용하지 않으면 자식 컴포넌트에서 ref를 직접 사용할 수 없기 때문에, dialog
요소를 제대로 제어할 수 없다import React, { useRef, forwardRef } from "react"; export default function Parent() { const dialogRef = useRef(); const handleClick = () => { if (dialogRef.current) { dialogRef.current.showModal(); // 모달을 열 때 showModal 사용 } }; return ( <div> <button onClick={handleClick}>Open Modal</button> <Modal ref={dialogRef} /> </div> ); } // forwardRef를 사용하여 부모로부터 ref를 전달받음 const Modal = forwardRef((props, ref) => { return ( <dialog ref={ref}> <h2>This is Modal</h2> <form method="dialog"> <p>Modal content goes here.</p> <button>Close</button> </form> </dialog> ); });
기존 모달 컴포넌트 함수를
forwardRef
로 감싼다. 그렇게 되면 기존 컴포넌트 함수에 인자로 원래 인자인 props, 제대로 전달받을 수 있게된 ref가 오게된다. 이렇게 만들면 모달띄우기 성공이다.
🤩EXTRA
useImperativeHandle을 통해 최적화
forwardRef
를 통해 부모 컴포넌트에서 자식 컴포넌트의 DOM 요소에 접근할 수 있지만, 가끔은 자식 컴포넌트가 부모에게 단순히 DOM 요소를 넘겨주는 것 외에, 특정 메서드나 기능만 노출하고 싶을 때가 있다. 이때 useImperativeHandle
을 사용하여 부모가 자식의 내부 상태나 로직에 불필요하게 접근하지 않도록 제어할 수 있다.import React, { useRef, forwardRef, useImperativeHandle } from "react"; // Parent 컴포넌트 export default function Parent() { const dialogRef = useRef(); const handleClick = () => { if (dialogRef.current) { dialogRef.current.openModal(); // openModal 메서드만 호출 가능 } }; const handleClose = () => { if (dialogRef.current) { dialogRef.current.closeModal(); // closeModal 메서드 호출 } }; return ( <div> <button onClick={handleClick}>Open Modal</button> <button onClick={handleClose}>Close Modal</button> <Modal ref={dialogRef} /> </div> ); } //forwardRef와 useImperativeHandle을 이용한 Modal 컴포넌트 const Modal = forwardRef((props, ref) => { const dialogRef = useRef(); useImperativeHandle(ref, () => ({ openModal() { if (dialogRef.current) { dialogRef.current.showModal(); } }, closeModal() { if (dialogRef.current) { dialogRef.current.close(); } } })); return ( <dialog ref={dialogRef}> <h2>This is Modal</h2> <form method="dialog"> <p>Modal content goes here.</p> <button>Close</button> </form> </dialog> ); });
부모 컴포넌트는
useImperativeHandle
로 설정된 ref에 접근하여useImperativeHandle
의 두 번째 인자인 함수의 반환값(즉, 커스텀 메서드들)을 참조한다. 실제로 dialog
요소에 접근하는 ref는 자식 컴포넌트 내에서 관리되며, 위 예시에서는 dialogRef
를 통해 제어된다.따라서 다음과 같은 효과를 줄 수 있다.
- 부모는 자식의 복잡한 내부 구현에 접근하지 않고, 필요한 기능만 호출할 수 있어 컴포넌트의 응집력이 높아진다.
- 컴포넌트가 더 안전해지고 최적화되며, 외부에서 자식 컴포넌트를 조작할 때 의도하지 않은 동작을 막을 수 있다.
추가적으로 내 블로그의 createPortal관련 포스트를 참고하면 더욱 개선시킬 수 있는 방법이 존재한다. 아래 링크를 클릭하면 해당 포스트로 이동한다.
🐜CONCLUSION

여기까지 따라오는게 쉽지는 않았다.
많은 웹사이트에서 구현되고 있고, 아무렇지 않게 사용하는 모달이지만 생각보다 그 원리와 기능을 알면 복잡할 수 있다. 하지만 한번 알아두면 평생 써먹을 수 있는 코드이기 때문에 정확히 짚고 넘어가야 될 것 같아서 정리용으로 포스트를 작성해둔다.