Skip to content

Conversation

jinhokim98
Copy link
Contributor

issue

구현 사항

모달 컴포넌트 구현

사용하기 쉽게 모달을 사용할 수 있도록 useModal 훅과 Modal 컴포넌트를 구현했습니다.
모달의 열림 닫힘 상태는 useModal이 책임지며 별도로 외부에서 state를 만들어 모달을 조건부 렌더링 할 필요가 없습니다.
Modal 컴포넌트는 모달 스타일을 정의한 컴포넌트입니다.

기능은 useModal에 전부 선언되어있고, 필요한 스타일은 Modal 컴포넌트를 불러 사용하면 됩니다.

useModal의 첫 번째 인자로 띄우고 싶은 모달 컴포넌트를 넣어줍니다.
옵션으로 두 가지를 받으며, 모달의 dimmed layer를 클릭했을 때 꺼짐 여부, 모달이 닫힐 때 실행되는 콜백함수를 추가로 넣어줄 수 있습니다.

export type ModalOption = {
  closeOnClickDimmedLayer?: boolean;
  onClose?: VoidFunction;
};

const modal = useModal<boolean>(
  <Modal>
    <h1>안녕</h1>
    <div className="flex flex-row gap-2">
      <Button size="xs" style="primary" onClick={() => modal.close(true)}>
        확인
      </Button>
      <Button size="xs" style="tertiary" onClick={() => modal.close(false)}>
        취소
      </Button>
    </div>
  </Modal>,
);

useModal 자세히

useModal은 총 다섯가지를 제공해줍니다.

  • open: 모달을 여는 메서드
  • close: 모달을 닫는 메서드 (resolve 호출)
  • closeWithReject: 모달을 닫으면서 reject를 호출하는 메서드
  • component: useModal 첫 번째 인자로 받은 모달 내부 컴포넌트
  • isOpened: 모달의 열림 상태 (거의 사용할 일 없을 듯하지만 혹시나 해서)

useModal은 내부적으로 프로미스를 사용합니다.
모달이 열릴 때 프로미스를 생성하고 모달이 닫힐 때 resolve를 호출해서 모달의 결과를 기다린 후 다음 액션을 실행할 수 있습니다.
위의 예시에서 modal.close(true)를 호출해서 모달을 종료하면 response에는 true가 console로 찍히게 됩니다.

const handleOpen = async () => {
  const response = await modal.open();
  console.log(response);
};

이렇게 설계한 이유는 모달을 열고 확인 버튼을 누를 때 api 호출이 일어날 일이 꽤 있을 것이라 생각했기 때문입니다.
저의 todo인 문서 충돌 기능도 그렇고, 이벤트 추가하기에도 확인을 누르면 추가 api가 호출되기 때문에 이렇게 관리하면 좋을 것 같아서 비동기를 활용해봤습니다.

구현 영상

2025-10-05.22-27-27.mp4

🫡 참고사항

@jinhokim98 jinhokim98 self-assigned this Oct 5, 2025
@jinhokim98 jinhokim98 added this to the v3.1.0 milestone Oct 5, 2025
@jinhokim98 jinhokim98 added 🖥️ FE Frontend ⚙️ feat feature labels Oct 5, 2025
@jinhokim98 jinhokim98 moved this to In Review in crew-wiki-7-FE Oct 5, 2025
Copy link
Contributor

@chosim-dvlpr chosim-dvlpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿠키! 프로미스를 활용하는 설계 너무 좋네요👏👏👏
한 가지 추가로 제안드리는 점은...esc를 눌렀을 때 모달이 꺼지는 키보드 이벤트도 있으면 좋을 것 같아요! (나중에 해도 됩니다😆)
고생하셨습니다~~!

Comment on lines +5 to +15
export const HideScroll = ({children}: PropsWithChildren) => {
useEffect(() => {
document.body.style.overflow = 'hidden';

return () => {
document.body.style.overflow = '';
};
}, []);

return <>{children}</>;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스크롤 처리까지 꼼꼼하게 구현하셨군용👍
그럼 모달이 열리는 동안에 배경 스크롤도 막히는 걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요! 뒤에 배경 스크롤이 막히는 것을 의도로 작성했습니다

Comment on lines 3 to 4
resolve!: (value: T | PromiseLike<T>) => void;
reject!: (reason?: unknown) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PromiseLike 타입으로 체이닝을 할 수 있군요! 배워갑니다~
추가로 궁금한 점이 있는데요,

  1. resolve와 reject에 ! 타입을 붙인 이유가 궁금해요.
  2. reason에 unknown타입을 설정한 이유가 궁금해요. unknown자체도 에러를 유연하게 처리할 수 있는 장점이 있지만 좀 더 안전하게 타입을 지정할 수 있는 방법은 없을지요..!

Copy link
Contributor Author

@jinhokim98 jinhokim98 Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. !을 붙인 이유는 붙이지 않았을 때 이 에러가 발생하기 때문입니다.
    Property 'resolve' has no initializer and is not definitely assigned in the constructor.ts(2564)

생성자에서 해당 프로퍼티가 초기화가 되지 않았다고 판단할 때 이 에러를 발생시킨다고 합니다.
실제로는 생성자를 호출할 때 resolve와 reject 초기화를 해주지만 TS는 이를 인지하지 못한다고 합니다.

그래서 이렇게 하면 에러가 발생하지 않아요..

export class ManualPromise<T> {
  promise: Promise<T>;
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason?: unknown) => void;

  constructor() {
    this.resolve = () => {};
    this.reject = () => {};
    this.promise = new Promise<T>((res, rej) => {
      this.resolve = res;
      this.reject = rej;
    });
  }
}

그러나 빈 함수를 매번 할당하는 것이 복잡하고 단순히 !만 붙이면 해결이 되어서 !로 대체했습니다.
더 자세한 글은 여기를 참고해주세요.

microsoft/TypeScript-Vue-Starter#36

  1. reason에서 올 수 있는 값이 무엇인지 몰라서, (에러 객체일지, 에러 코드일지 에러 메시지일지) unknown으로 설정했어요.
    만약 팀에서 reason에는 에러 객체만 받을거야. 아니면 에러 메시지만 받을거야. 결정이 된다면 특정 타입을 단언해서 사용할 수도 있을 것 같아요. 이거에 대해서는 프룬은 어떻게 생각하시는지 궁금합니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 👍👍
  2. 제 생각에는 에러 객체가 좋을 것 같아요!
    에러가 발생한 경우 Sentry가 캐치하는데요, Error 객체에 쌓인 스택 트레이스 정보들을 통해서 어디서 에러가 발생하는지 확인할 수 있어요. 그래서 디버깅을 위해서 에러 객체만을 사용하는 것을 제안합니다~!

Copy link
Contributor Author

@jinhokim98 jinhokim98 Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다. Error 객체로 받도록 변경했습니다~
38becef

onClick={(event: MouseEvent) => {
event.stopPropagation();
if (closeOnClickDimmedLayer && event.target === event.currentTarget) {
close(undefined);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드를 읽다가 생긴 단순한 궁금증인데,, undefined를 명시적으로 넘긴 이유가 있는지 궁금해요~!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모달 내 확인 버튼, 취소 버튼을 사용해서 (open, close) 모달을 닫지 않고 다른 예외의 방식으로 모달을 이탈했기 때문에 undefined라고 설정했습니다!

Comment on lines 49 to 55
onClick={(event: MouseEvent) => {
event.stopPropagation();
if (closeOnClickDimmedLayer && event.target === event.currentTarget) {
close(undefined);
}
}}
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onClick 핸들러를 분리해보면 어떨까요?!

Copy link
Contributor Author

@jinhokim98 jinhokim98 Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(궁금) 이를 분리하자면 handleOnClick 메서드를 만들어서 주입하는 방식을 말씀하시는걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다~ 71503b7

Comment on lines +8 to +26
const modal = useModal<boolean>(
<Modal>
<h1>안녕</h1>
<div className="flex flex-row gap-2">
<Button size="xs" style="primary" onClick={() => modal.close(true)}>
확인
</Button>
<Button size="xs" style="tertiary" onClick={() => modal.close(false)}>
취소
</Button>
</div>
</Modal>,
{
closeOnClickDimmedLayer: true,
onClose: () => {
console.log('모달 종료');
},
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 그런데 요 페이지는 뭘까요?! 테스트 코드인데 잘못 들어간 것인지..!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이 페이지는 모달 사용법을 남기기 위해서 추가해둔 것입니다.
통계 페이지인데, 지금 구현이 되어있지 않아서 임시로 사용했어요.

@jinhokim98
Copy link
Contributor Author

ESC 입력 시 모달이 꺼지는 기능도 있으면 좋을 것 같아요. 감사합니다~
옵션으로 추가해두었어요.
66a2201

@jinhokim98 jinhokim98 modified the milestones: v3.0.3, v3.1.0 Oct 12, 2025
@jinhokim98 jinhokim98 changed the base branch from develop2 to develop October 12, 2025 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

2 participants