REACT 모달 애니메이션(CSS)

미누도 개발한다·2021년 9월 16일
4

리액트

목록 보기
9/11

모달이 Fade In, Fade Out 되는 것을 구현하고자 합니다.

기본코드세팅은 아래와 같습니다.

App.js

import React, { Component } from "react";

import "./App.css";
import Modal from "./components/Modal/Modal";
import Backdrop from "./components/Backdrop/Backdrop";
import List from "./components/List/List";

class App extends Component {
  state = {
    showModal:false
  }
   
  openModal = () => {  
    this.setState({showModal:true});
  }
  closeModal=() => {
    this.setState({showModal:false});
  }

  render() {
     
    return (
      <div className="App"> 
        <h1>React Animations</h1> 
        <Modal show={this.state.showModal} closed ={this.closeModal}  />
        <Backdrop show ={this.state.showModal}/>
        <button className="Button" onClick={this.openModal}>Open Modal</button>
        <h3>Animating Lists</h3>
        <List />
      </div>
    );
  }
}

export default App;

Modal.js

import React from "react";

import "./Modal.css";

function Modal(props) {
    const styleClasses = ["Modal",props.show? "ModalOpen":"ModalClose"]
  return (
    <div className={styleClasses.join(' ')}>
      <h1>A Modal</h1>
      <button className="Button" onClick={props.closed}>
        Dismiss
      </button>
    </div>
  );
}

export default Modal;

Backdrop.js

import React from 'react';

import './Backdrop.css';

function Backdrop(props) {
    const styleClasses = ["Backdrop",props.show? "BackdropOpen":"BackdropClose"]
    return (
        <div className={styleClasses.join(' ')}></div>
    )
}

export default Backdrop

Modal.css

.Modal {
    position: fixed;
    z-index: 200;
    border: 1px solid #eee;
    box-shadow: 0 2px 2px #ccc;
    background-color: white;
    padding: 10px;
    text-align: center;
    box-sizing: border-box;
    top: 30%;
    left: 25%;
    width: 50%;
    transition: all 0.2s ease-out;
}
.ModalOpen {
    display:block
    
}
.ModalClose {
    display:none
    
}


.Backdrop.css

.Backdrop {
    position: fixed;
    z-index: 100;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.8);
}

.BackdropOpen {
    display:block;
}

.BackdropClose {
    display:none;
}

시도1 display 적용

App 컴포넌트에서 버튼을클릭하면, showModal 상태가 바뀌면서, 모달과 백드롭을 띄울지 말지 결정하게 됩니다.

아마 모달의 transition이 안먹을 것입니다.

display none 이 transition을 안먹는 이유:

https://velog.io/@dev-tinkerbell/display-none%EC%9D%B4-transition%EC%9D%B4-%EC%95%88%EB%A8%B9%ED%9E%88%EB%8A%94-%EC%9D%B4%EC%9C%A0

시도2 opacity 적용

display가 안되니, opacity를 적용해보겠습니다.

{...}

.ModalOpen {
	opacity:1
    
}
.ModalClose {
	opacity:0
    pointer-events: none;     
}

하지만 , opacity를0 으로 지정하면, Close된 상태임에도 불구하고

렌더트리에 존재하고, 화면상에 그려집니다.

따라서 아래에 깔린 화면들의 버튼이 눌리지않거나 동작하지 않을수도 있죠.

따라서, pointer-events 속성을 none으로 지정하면 동작은 정상적으로 됩니다.

시도3 조건부 렌더링

하지만 맘에 들지않는 부분이, 모달이 보여지든 ,안보여지든 모달컴포넌트와 백드롭 컴포넌트가 렌더링 되어서 DOM화면을보면 사이즈가 0인채로 브라우져 돔에 떠있습니다. 이는 화면이 많고 복잡해지는 경우, 버그를 유발할수있고, 쓸데없는 메모리 낭비이죠. 따라서 App 컴포넌트에서 조건부 렌더링을 통해, Modal과 Backdrop을 다뤄보겠습니다.

Modal.css 만 수정해보았습니다. keyframe을 추가하고 애니메이션을 적용해주었습니다.

App.js

import React, { Component } from "react";

import "./App.css";
import Modal from "./components/Modal/Modal";
import Backdrop from "./components/Backdrop/Backdrop";
import List from "./components/List/List";
import { Transition } from 'react-transition-group';

class App extends Component {

  state = {
    showModal:false
  }
  openModal = () => {  
    this.setState({showModal:true});
  }
  closeModal=() => {
    this.setState({showModal:false});
  }

  render() {
     
    return (
      <div className="App"> 
        <h1>React Animations</h1> 
        {this.state.showModal ? <Modal show ={this.state.showModal}/>:null}
        {this.state.showModal ? <Backdrop show ={this.state.showModal}/>:null}
        <button className="Button" onClick={this.openModal}>Open Modal</button>
        <h3>Animating Lists</h3>
        <List />
      </div>
    );
  }
}

export default App;

Modal.css

.Modal {
    position: fixed;
    z-index: 200;
    border: 1px solid #eee;
    box-shadow: 0 2px 2px #ccc;
    background-color: white;
    padding: 10px;
    text-align: center;
    box-sizing: border-box;
    top: 30%;
    left: 25%;
    width: 50%;
    transition: all 0.3s ease-out;
}
.ModalOpen {
    opacity: 1;   
    animation: FadeIn .3s ease-in;
}
.ModalClose {
    
    opacity: 0;
    pointer-events: none;
    animation: FadeOut .3s ease-out;
}

@keyframes FadeIn {
    0% {
        opacity: 0;
    }
    1%{
        opacity: 0;
        transform:translateY(100%)
    }
    100% {
        opacity: 1;
    }
}

@keyframes FadeOut {
    0% {
        opacity:1
    }
    1% {
        opacity:1;
    }
    100% {
        opacity:0;
    }
}

돌려보면, fade-In 은 되는데, fade-out이 안되고 휙 사라져 버릴것입니다.

그이유는,

state의 showModal =false 가 되는순간, 리렌더링이 발생하고, 실제돔에 반영이 되는데, modal, backdrop 돔객체가 삭제 되어버리니, fade-out animation이 적용될 수가 없습니다.

즉, 타겟 돔객체가 없다가 새로 생기면, keyframe에 의해 적용이 되는데,

있다가 없으면 렌더트리에서 사라지므로 애니메이션 자체가 적용이 안된다는 의미입니다.

시도4 Transition 컴포넌트 사용

참고 : https://reactcommunity.org/react-transition-group/transition

Transition 컴포넌트를 이용하면,리렌더링시 돔객체의 존재 유/무로 CSS가 안먹을때 한계를 극복할 수 있습니다. 동작설명은 공식문서에 쉽게 나와있으니 참고해주세요.

동작원리를 유추하면, Transition 컴포넌트를 항상 렌더링 하고, (브라우저 요소검사에서는 보이지 않습니다.) 하위에 들어가는 컴포넌트를 4가지 상태값(exited,exiting,entered,entering)에 따라 timeout을 통해 리렌더링 시킵니다. exit,enter상태 에 따라 하위 컴포넌트를 동적으로 unmount, mount 시킬 수 있습니다.

하위 컴포넌트를 exit 상태에 둘지, enter상태에 둘지는 transition 컴포넌트의 in 속성을 이용합니다. exiting -> exit 로 리렌더링될때는 해당 자식 컴포넌트가 unmount 되도록, unmountOnExit 를 반드시 설정해주어야 합니다.(의도한 동작이 모달이 닫히면, 돔에 남지않도록 unmount 해야하기 때문 입니다.
Transition 컴포넌트의 unmountOnExit을 따로 설정하지않으면, exit 상태일때도 하위 컴포넌트를 그대로 렌더링 합니다. 쉽게 비유하자면, CSS로 컨트롤 하고싶은 컴포넌트의 렌더링을 Transition에게 위임하는 것이죠)

완성코드는 아래를 참고해주세요. 이쁘게 잘 동작합니다!
App.js

import React, { Component } from "react";

import "./App.css";
import Modal from "./components/Modal/Modal";
import Backdrop from "./components/Backdrop/Backdrop";
import List from "./components/List/List";
import { Transition } from 'react-transition-group';

class App extends Component {
  state = {
    showModal:false
  }
  openModal = () => {  
    this.setState({showModal:true});
  }
  closeModal=() => {
    this.setState({showModal:false});
  }

  render() {
     
    return (
      <div className="App"> 
        <h1>React Animations</h1> 
        <Transition unmountOnExit in={this.state.showModal} timeout={500}>
        {state => (
          <Modal show={state} closed ={this.closeModal}  />
        )}
      </Transition>
        
        
        {this.state.showModal ? <Backdrop show ={this.state.showModal}/>:null}
        <button className="Button" onClick={this.openModal}>Open Modal</button>
        <h3>Animating Lists</h3>
        <List />
      </div>
    );
  }
}

export default App;

Modal.js

import React from "react";

import "./Modal.css";

function Modal(props) {
    const styleClasses = ["Modal",props.show === 'entering'? "ModalOpen" : props.show === 'exiting' ? 'ModalClose':null]
  return (
    <div className={styleClasses.join(' ')}>
      <h1>A Modal</h1>
      <button className="Button" onClick={props.closed}>
        Dismiss
      </button>
    </div>
  );
}

export default Modal;

글을 이쁘게 써야하는데, 설명만 줄글로 나열하느라 보기가 편하진 않네요,
오늘은 여기까지! 👏

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

1개의 댓글

comment-user-thumbnail
2023년 1월 9일

유용하게 배워갑니다~

답글 달기