Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 공용 모달 컴포넌트 추가 #11

Merged
merged 13 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/components/Modal/Modal.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { style } from '@vanilla-extract/css';

export const background = style({
width: '100vw',
height: '100vh',
zIndex: 100,

position: 'fixed',
top: '0px',
left: '0px',

display: 'flex',
justifyContent: 'center',
alignItems: 'center',

backgroundColor: 'rgba(25, 25, 27, 0.3)',
});

export const container = style({
width: '326px',
padding: '24px',

display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '2.4rem',

backgroundColor: '#fff',

borderRadius: '8px',
});
35 changes: 35 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { FormEvent, MouseEventHandler, ReactNode, useEffect, useRef, useState } from 'react';
seoyoung-min marked this conversation as resolved.
Show resolved Hide resolved
import ModalPortal from '@/components/ModalPortal';
import * as styles from './Modal.css';
import ModalTitle from './ModalTitle';
import ModalButton from './ModalButton';
import useOnClickOutside from '@/hooks/useOnClickOutside';
import useBooleanOutput from '@/hooks/useBooleanOutput';

interface ModalMainProps {
children?: ReactNode;
handleModalClose: () => void;
}

function ModalMain({ children, handleModalClose }: ModalMainProps) {
const { ref } = useOnClickOutside(() => {
handleModalClose();
});
Copy link
Contributor Author

@seoyoung-min seoyoung-min Feb 2, 2024

Choose a reason for hiding this comment

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

@Nahyun-Kang
이 부분이 추가 됐습니다! 속성값으로 검정바탕 눌렀을때 발생할 핸들러 전달주시면 됩니다~! :)

Copy link
Contributor

Choose a reason for hiding this comment

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

확인했습니다!! :D 알려주셔서 감사해용!! 🙇‍♀️


return (
<ModalPortal>
<div className={styles.background}>
<div ref={ref} className={styles.container}>
{children}
</div>
</div>
</ModalPortal>
);
}

const Modal = Object.assign(ModalMain, {
Title: ModalTitle,
Button: ModalButton,
});

export default Modal;
41 changes: 41 additions & 0 deletions src/components/Modal/ModalButton.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { style } from '@vanilla-extract/css';

export const buttonContainer = style({
width: '100%',

display: 'flex',
justifyContent: 'flex-end',
gap: '16px',
});

export const baseButton = style({
padding: '12px 16px',

display: 'flex',
justifyContent: 'center',
alignItems: 'center',

flexShrink: '0',

borderRadius: '12px',
fontSize: '1.4rem',
fontWeight: '500',
lineHeight: '20px',
letterSpacing: '-0.4px',
});

export const primaryButton = style([
baseButton,
{
backgroundColor: '#0047FF',
color: '#fff',
},
]);

export const secondaryButton = style([
baseButton,
{
backgroundColor: '#EBF4FF',
color: '#0047FF',
},
]);
21 changes: 21 additions & 0 deletions src/components/Modal/ModalButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MouseEventHandler, ReactNode } from 'react';
import * as styles from './ModalButton.css';

interface ModalButtonProps {
children: ReactNode;
onCancel: MouseEventHandler<HTMLButtonElement>;
onClick: MouseEventHandler<HTMLButtonElement>;
Copy link
Contributor

Choose a reason for hiding this comment

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

이벤트핸들러 타입을 지정하실 때 핸들러 자체 타입으로 지정하신 이유가 궁금합니다!!

  onCancel: (e: MouseEvent<HTMLButtonElement>) => void;
  onClick: (e: MouseEventHandler<HTMLButtonElement>) => void;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

사실 타입이 너무 어려워서, 오류가 뜨면 수정해나가면서 하고 있는데요!🥲

button 태그의 onClick 타입이 핸들러 자체 타입인 것 같아 그대로 적었습니다🙈

Screenshot 2024-02-03 at 03 36 01

핸들러 자체 타입 지정 대신 다른 타입으로 지정하는 것이 좋을까요??🤔

}

export default function ModalButton({ children, onCancel, onClick }: ModalButtonProps) {
return (
<div className={styles.buttonContainer}>
<button type="button" className={styles.secondaryButton} onClick={onCancel}>
취소
</button>
<button type="button" className={styles.primaryButton} onClick={onClick}>
{children}
</button>
</div>
);
}
8 changes: 8 additions & 0 deletions src/components/Modal/ModalTitle.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';

export const title = style({
width: '100%',
fontSize: '1.6rem',
fontWeight: '600',
lineHeight: '150%',
});
6 changes: 6 additions & 0 deletions src/components/Modal/ModalTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ReactNode } from 'react';
import * as styles from './ModalTitle.css';

export default function ModalTitle({ children }: { children: ReactNode }) {
return <div className={styles.title}>{children}</div>;
}
14 changes: 14 additions & 0 deletions src/components/ModalPortal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ReactNode } from 'react';
import ReactDOM from 'react-dom';

Copy link
Contributor

Choose a reason for hiding this comment

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

서영님 이 부분은 사소하지만 createPortal을 바로 불러와도 좋을 것 같습니다.
또, 모달이 브라우저 상태일때만 렌더링될 수 있도록 체크하는 조건을 넣어주는 방법도 불필요한 렌더링을 막을 수 있어서 참고로 말씀드려 봅니다!!

import { createPortal } from "react-dom";

const ModalPortal = ({ children }: Props) => {
  /** 모달이 서버상태에서는 실행되지 않도록 */
  if (typeof window === "undefined") {
    return null;
  }

  const el = document.getElementById('modal-root') as HTMLElement;

  return createPortal(children, el);
};

export default ModalPortal;

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.

꼼꼼하게 봐주셔서 감사합니다 :)

interface Props {
children: ReactNode;
}

const ModalPortal = ({ children }: Props) => {
const el = document.getElementById('modal-root') as HTMLElement;

return ReactDOM.createPortal(children, el);
};

export default ModalPortal;
22 changes: 22 additions & 0 deletions src/hooks/useBooleanOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useCallback, useEffect, useState } from 'react';

interface useBooleanOutput {
isOn: boolean;
toggle: () => void;
handleSetOn: () => void;
handleSetOff: () => void;
}

export default function useBooleanOutput(defaultValue?: boolean): useBooleanOutput {
const [isOn, setIsOn] = useState<boolean>(!!defaultValue);

const toggle = useCallback(() => setIsOn((prev) => !prev), []);
const handleSetOn = useCallback(() => setIsOn(true), []);
const handleSetOff = useCallback(() => setIsOn(false), []);

useEffect(() => {
setIsOn(false);
}, []);

return { isOn, toggle, handleSetOn, handleSetOff };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

서영님 토글을 useCallback으로 감싸기 너무 좋네요!!
매번 열림, 닫힘 코드를 작성하지 않고 상태를 관리하는 훅 너무 좋은 방법이네요!!!

  1. isOn 초기값이 boolean으로 추론될 것 같아서 제네릭 타입을 지정하지 않아도 될 것 같습니다.
const [isOn, setIsOn] = useState(!!defaultValue);
  1. 궁금한점이 useEffect는 isOn 상태가 무엇이든지 false로 만들어주는 역할을 하고 있는 것이 맞나용?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. 우왓 boolean 값 추론이 되겠군요!! 꼼꼼하게 봐주셔서 감사합니다🙇‍♀️

  2. 네 맞습니다!! 그런데 이러면 defaultValue 의미가 없어지는군요ㅠㅠ

추가했던 이유는 이전에 useToggle 훅을 만들어 모달을 관리한 적이 있는데 vercel 배포과정에서 strictmode가 꺼지면서 모든 모달이 오픈되는 오류가 발생했었습니다. (이유를 정확히는 모르겠지만) 렌더링이 되면서 뭔가 잘못되는 것 같아, 저 코드를 넣었더니 해결이 됐습니다 🤔

(1) 정확한 원인도 모르는 해결책이니 우선 지우고 문제 발생시 다시 생각해본다.
(2) 혹시를 대비하여 아래처럼 코드를 수정한다

  useEffect(() => {
    if (!!defaultValue === false) {
      setIsOn(false);
    }
  }, []);

저는 1번으로 제거 해볼까 하는데, 소현님 의견도 궁금합니다 :)

23 changes: 23 additions & 0 deletions src/hooks/useOnClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useRef, useState } from 'react';

const useOnClickOutside = (handler: () => void) => {
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.

꺄 감사합니다 나현님🙇‍♀️ 사실 저번 팀프로젝트들에서 얻은 것들을 주섬주섬 주워다가 쓰고 있습니다 ㅎㅎ

const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (e: Event) => {
if (ref.current !== null && !ref.current.contains(e.target as Node)) {
handler();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

오! 매번 outSideClick 함수를 버블링으로만 구현했었는데 이렇게 ref를 이용하는 방법도 있군요!
이 방법이 팝오버 메뉴에 적용할 때 더 유용할 것 같다는 생각이 드네요, 또 서영님 덕분에 touchstart 이벤트도 알아갑니당👍

Copy link
Contributor

Choose a reason for hiding this comment

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

궁금해서 찾아보니 touchstart가 모바일/태블릿 기기 전용 이벤트 였군요!! 서영님 세심함에 감탄하고 갑니다 ㅎㅎ 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

헉 감사합니다 🙇‍♀️

저는 항상 ref로만 구현 해봐서, 버블링으로 구현하는 방법 너무 궁금합니다! 소현님 이전 프로젝트 구경가야겠어요 🏃‍♀️

}, [ref]);

return { ref };
};

export default useOnClickOutside;