본문 바로가기

Styled Components로 Modal 만들기

Styled Components로 Modal 만들기

 

*  React, Styled-Component, Storybook을 활용한 UI 컴포넌트 개발 
*  Styled Components를 활용해 Modal 커스텀 컴포넌트를 구현 
*  Storybook을 사용한 컴포넌트 관리 

 

Modal Component 

 

완성 예시

 

 

Modal UI 컴포넌트는 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것을 말한다.

팝업 창: 현재 열려있는 브라우저 페이지에 또 다른 브라우저 페이지를 띄우는 것
             (브라우저에서 이 창을 열고 닫기 제어가 가능) 
모달 창: 현재 열려있는 브라우저 페이지에 레이어를 까는 것
             (기존의 페이지와 부모-자식의 관계로 브라우저의 새 창 제어 옵션에 전혀 영향을 받지 않는다.)

-> 팝업 창은 기존의 웹 페이지와 분리된 상태에서 작업을 하므로 재활용이 가능하며, 페이지별 이동이 자유롭다.
-> 따라서 팝업 창과 모달 창의 장점만을 가져오기 위해서는, iframe을 통해 모달 창 생성을 하면 된다.
         예: window.open("popup.html", "", "width=300, height=300");

 

필요한 컴포넌트 

ModalContainer (div) : Modal을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 

  • ModalBtn (button) : Modal 창을 켜고 끌 수 있는 버튼 
  • ModalBackdrop (div) : Modal이 떴을 때의 배경
    • ModalView (div) : Modal 창 
      • ExitBtn (button) : Modal 창 내부에 있는 X 버튼 

 

state

Modal 컴포넌트는 아래와 같은 state가 존재한다.

  • isOpen state : 모달 창의 열고 닫힘 여부를 확인 

모달 창 제어를 위한 핸들러 함수 openModalHandler는 모달 창을 여는 버튼, 모달 창의 X 버튼, 모달 창 바깥 배경 레이어에 넣어준다. 

  • 즉, 모달 창 버튼을 클릭해서 모달 창을 열고 모달 창 바깥 배경 혹은 모달 창 내부의 X버튼을 눌러 닫는다.
  • openModalHandler 함수 :
    • ModalBtn 클릭 시 발생되는 change 이벤트 핸들러 
    • 클릭할 때마다 상태가 Boolean 값으로 변경 

 

조건부 렌더링

ModalBtn을 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메서드가 실행되어야 한다.

  • 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만
    • 모달창(ModalView)과 
    • 배경(ModalBackdrop)이 뜰 수 있게 구현한다.
  • 조건부 렌더링을 활용해서
    • Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!'로
    • Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn의 내부 텍스트가 'Open Modal'이 되도록 구현한다.

 

구현 코드 

 

Modal UI Component 코드
import { useState } from 'react';
import styled from 'styled-components';

export const ModalContainer = styled.div`
  height: 100%;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
`;

// Modal이 떴을 때의 배경
export const ModalBackdrop = styled.div`
  background-color: rgba(0,0,0,0.2);     // 투명도 0.2
  position: fixed;                       // 위치 고정 
  top : 0;                               // 화면을 꽉 채움
  left : 0;
  right : 0;
  bottom : 0;
  display: flex;                         // 자식요소인 모달 창 중앙으로 배치
  justify-content: center;
  align-items: center;

`;

export const ModalBtn = styled.button`
  background-color: var(--coz-purple-500);
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;                         // 버튼에 마우스를 갖다대면 커서 아이콘 변경
`;

// 모달창 -> attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있음
// (시맨틱한 마크업을 위해 role을 사용)
export const ModalView = styled.div.attrs((props) => ({       
  role: 'dialog',
}))`
  height: 200px;
  width: 300px;
  background-color: skyblue;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  > div.msg {                 // ModalView의 자식요소 CSS
    font-size: 1.7rem;
    color : #475ed4;
  }
`;

// Styled Components로 만든 컴포넌트를 재사용 하려면 styled(가져올 컴포넌트 이름)으로 작성
export const ExitBtn = styled(ModalBtn)`
  width: 5px;
  height: 5px;
  position: absolute;
  right: 10px;
  top: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
`

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = () => {
    setIsOpen(!isOpen);
  };
 return (
    <>
      <ModalContainer>
          <!-- (1) 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는
          ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는
          ModalBtn 의 내부 텍스트가 'Open Modal'이 된다. -->
        
          <ModalBtn onClick={openModalHandler}>
          { isOpen ? "Opened" : "Open Modal" } 
          </ModalBtn> 

          <!-- (2) 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜬다. -->
          
          {isOpen ? 
          
          <ModalBackdrop onClick={openModalHandler}>
            <ModalView onClick={(e) => e.stopPropagation()}>  
              <Exit onClick={openModalHandler}>  
              &times;
              </Exit>
              <div className = "msg">
              Have a nice day 🤍
              </div>
            </ModalView>
          </ModalBackdrop> 
          
          : null }

      </ModalContainer>
    </>
  );
};

 

포인트

 

// 예시 코드
<ModalBackdrop onClick={openModalHandler}>
   <ModalView>
        <div> 모달창이 떴습니다! </div>
   </ModalView>
</ModalBackdrop>

🟣 이벤트 버블링 :

  • 모달창을 눌렀을 때는 모달창이 닫히면 안 된다.
  • 위의 예시 코드에서 모달창(ModalView) 컴포넌트에 onClick 이벤트가 없음에도 불구하고, 해당 코드를 실행시켜보면 모달창만 눌러도 모달창이 닫힌다.
  • 그 이유는, 모달창의 배경 레이어인 ModalBackdrop이 눌렀기 때문이며, 이를 이벤트 버블링이라고 한다.
    • 이벤트 버블링(Bubbling)이란, 하위요소에서 상위요소로 이벤트(이 경우, 마우스로 클릭..!!)가 전파되어가는 방식이다.
    • 자식 요소에 발생한 이벤트가 상위의 부모요소까지 영향을 미치는 것이다.
  • 버블링을 막기 위해 event.stopPropagation()을 Click 이벤트로 설정해준다.
    • 이벤트 버블링은 target요소에서 상위로 올라가 html 태그, document, window까지 전달된다.
    • 이를 막기 위한 메소드 event.stopPropagation을 이벤트 객체에서 꺼내 쓴다.
    • 참고로, 버블링의 반대는 캡쳐링(상위 부모요소로부터 하위로)이며 이를 막기 위한 메소드도 동일하다.
<ModalView onClick={(event) => event.stopPropagation()}>

 

🟣 attrs 메소드 : 엘리먼트에 속성을 추가

export const ModalView = styled.div.attrs((props) => ({     
  role: 'dialog',
}))
  • 아래와 같이 div에 role 속성이 추가된다.

 

  • 아래와 같이 컴포넌트 내에 속성을 추가한 것과 동일하다.
 <ModalView name="anotherOne" onClick={(e) => e.stopPropagation()}>

 

심플 코드 (참고용)
더보기

위의 코드와 차이점은 isOpen의 값에 따라서 아래 코드는 True일 때는 모달창만, False일 때는 버튼만 구현했다.

(위의 코드는 isOpen 값이 True이면 모달창 + Opened 버튼 + 배경 레이어 / False일 때는 Open 버튼) 

 

import React, { useState } from 'react';
import styled from 'styled-components';

export const Container = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
`;

export const ModalEl = styled.div`
  width: 300px;
  height: 300px;
  background-color: blue;
  text-align: center;
  padding: 15px;
  border-radius: 20px;
`;

export const Button = styled.button`
  background-color: blue;
  color: white;
  border: none;
  border-radius: 15px;
  padding: 10px 20px;
  cursor: pointer;
`;

export const ExitButton = styled(Button)` 
  background-color: white; 
  color: blue;
  padding: 5px 10px;
  margin-bottom: 85px;
`;

export const Text = styled.h1`
  color: white;
  font-size: 40px;
  font-weight: bold;
`;


export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const handleClick = () => {
    setIsOpen(!isOpen); // 매 클릭마다 기존 state의 반대 값을 반복적으로 설정 
  };

  return (
    <Container>
        {isOpen ? (
          <ModalEl>
            <ExitButton onClick={handleClick}>X</ExitButton>
            <Text>Have a good day :) </Text>
          </ModalEl>
        ) : <Button onClick={handleClick}>Open</Button> }
    </Container>
  );
}

 

Storybook 컴포넌트 관리

 

 

카테고리 관리

import React from 'react';
import '../variables.css';
import { Modal } from '../components/BareMinimumRequirements/Modal';

export default {
  title: 'UI/Modal',
  component: Modal,
  argTypes: {
    title: { control: "text" },
    color: {control: "color"}
}
};

 

  • UI/ Modal로 모달창 컴포넌트가 정리되어 들어가 있다.

 

아래와 같이 Storybook에서 색감 조정하기

직접 props를 받을 수 있도록 코드를 작성한다. (storybook 포스팅 참고)

 

Modal.stories.js
const Template = (args) => <Modal {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Modal'
};


export const Practice = (args) =>{
  return <Modal {...args} />
}

 

Modal.js
// 모달 버튼 바탕색 props.color 설정
export const ModalBtn = styled.div`
    background-color: ${(props) => props.color || "var(--coz-purple-500)"}; 
 .
 .
 .
 `
 
 .
 .
 .
 
 // 모달 버튼 color 속성 추가

 <ModalBtn color={color} onClick={openModalHandler}>
          { isOpen ? "Opened" : "Open Modal" } 
          </ModalBtn>

 

 

728x90
⬆︎