React Modal 만들기

chu·2021년 4월 5일
1

현재 투두앱으로 포트폴리오를 만들고 있는 중이었다. 단순히 리스트를 추가하고, 삭제, 수정만이 하는게 아니라, 노드로 백엔드까지 만들어서 진행하려고 했으나...

생각보다 컴포넌츠는 나누는 작업이 많았고, 회원가입 및 로그인까지 작업을 해야되서 첫 포폴치고는 작업량이 많았다.

그래서 일단은 투두앱, 회원가입 및 로그인 이 두 가지로 나누기로 하였다. 그 전에 작업에 많이 쓰이는 기술을 먼저 정리하려고 한다.

첫번째로 팝업 모달이다.

코드는 깃헙에서 확인 가능합니다.


팝업 모달은 스위치처럼 on / off 할 수 있는 팝업 창이다.
간단하게는 버튼을 클릭하여 팝업창이 뜨고, X 버튼을 눌러 닫는 것이다. 그 외에 X 버튼 뿐만 아니라, 그 외 영역을 클릭 시에도 팝업창을
닫게 만드는 것이다.

아마 사이트에서는 없어서는 안되는 기능일거다.
한번 정리를 해보자!

이 작업에서는 webpack typescript emotion을 사용하여
작업을 진행하였다.

webpack.config.ts

const webpack = require('webpack');
const path = require('path');
const ForktsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// 배포용이 아닐경우 -> 개발용
const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = {
  name: 'popup',
  mode: 'isDevelopment',
  devtool: !isDevelopment ? 'hidden-source-map' : 'eval',
  entry: {
    app: './client',
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'babel-loader',
        options: {
          presets: [
            [
              '@babel/preset-env',
              {
                targets: { browsers: ['last 2 chrome versions'] },
              },
            ],
            '@babel/preset-react',
            '@babel/preset-typescript',
          ],
          env: {
            development: {
              plugins: [
                ['@emotion', { sourceMap: true }],
                require.resolve('react-refresh/babel'),
              ],
            },
            production: {
              plugins: ['@emotion'],
            },
          },
        },
        exclude: path.join(__dirname, 'node_modules'),
      },
      {
        test: /\.css?$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.jsx', '.json'],
  },
  plugins: [
    new ForktsCheckerWebpackPlugin({
      async: false,
    }),
    new ReactRefreshWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin(),
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
  },
  devServer: {
    port: 3050,
  },
};

위 작업을 하면서 거의 1시간가량 시간을 잡아먹고 설정을 했다.
하지만 설정이 만족스럽진 않다. 처음에는 module.exports가 아닌 const config: webpack.Configuration = {}로 진행했지만, 계속 오류가 발생해서 module.exports로 진행을 했고,
require 대신 import를 썼지만, 이 또한 에러가 계속 발생되서 결국 require로 진행을 하게 되었다.

webpack 설정

mode - 개발용 또는 배포용 어떤 환경으로 진행할지 정하면 된다.

devtool - 디버깅 프로세스를 향상 시키는 영역이며, 개발시에는 eval , 배포용에는 hidden-source-map을 사용한다.

entry - 진입할 파일 (SPA는 하나의 진입점만 넣음)

output - 진입을 하게 된다면, 당연히 결과물도 있겠죠. 그 결과물을 어떻게 내보낼지에 대한 영역입니다. path는 결과물이 있는 디렉토리를 말합니다. filename은 결과물의 파일명이며, 여기서 [name]은 entry에서 app을 말합니다. 그래서 실제로 결과물은 app.js가 나오죠.

추후 나머지 부분은 더 정리하도록 하겠습니다.

tsconfig.json

{
  "compilerOptions": {
    "esModuleInterop": true, // true로 고정
    "sourceMap": true, // 소스맵 true
    "lib": ["ES2020", "DOM"],
    "jsx": "react", // react-jsx로 설정 시 오류 발생
    "module": "esnext",
    "moduleResolution": "Node",
    "target": "es5", // es5까지 호환할 수 있도록
    "strict": true, // 타입 강하게 체크
    "resolveJsonModule": true,
    "baseUrl": "."
  }
}

tsconfig-for-wepback-config.json

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "Node",
    "target": "es5",
    "esModuleInterop": true
  }
}

tsconfig 설정 또한 추후 추가적으로 정리하도록 하겠습니다.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/dist/app.js"></script>
  </body>
</html>

html 영역은 흔히 보는 패턴입니다. 여기서 scriptsrc를 보면 webpack 설정에서 output의 경로를 적어주시면 됩니다.

client.tsx

webpack 설정에서 entry에 있는 app: client.tsx부분의
파일영역입니다.

import React from 'react';
import { render } from 'react-dom';

import App from './components/App/index';

render(<App />, document.querySelector('#root'));

이 부분 또한 흔히 보는 영역이라, 설명은 없습니다.
이제 팝업 모달에 대해서 정리하겠습니다.

App.tsx

import React, { useState, useCallback, useRef, useEffect } from 'react';
// Font Awesome icon
import { FaAlignJustify } from 'react-icons/fa';
import styled from '@emotion/styled';
import Modal from '../modal';

const Container = styled.div`
  //.. style
`;

const App = () => {
  const [show, setShow] = useState(false);
  const popRef = useRef<HTMLDivElement>(null);

  const onClickOutside = useCallback(
    ({ target }) => {
      if (popRef.current && !popRef.current.contains(target)) {
        setShow(false);
      }
    },
    [setShow]
  );

  useEffect(() => {
    document.addEventListener('click', onClickOutside);
    return () => {
      document.removeEventListener('click', onClickOutside);
    };
  }, []);

  const onClickToggleModal = useCallback(() => {
    setShow(prev => !prev);
  }, [setShow]);

  return (
    <Container>
      <div ref={popRef}>
        <FaAlignJustify onClick={onClickToggleModal} />
        <Modal show={show} />
      </div>
    </Container>
  );
};

export default App;

Modal.tsx

import React, { FC, useRef, useEffect, useCallback, useState } from 'react';
import styled from '@emotion/styled';

const MenuBox = styled.ul`
  // ..style
`;

interface Props {
  show: boolean;
}

const Modal: FC<Props> = ({ show }) => {
  // show가 false면 화면에 메뉴를 나타내지 않는다.
  if (!show) {
    return null;
  }
  // show가 true면 아래 메뉴가 화면에 나타난다.
  return (
    <MenuBox>
      <li>마이페이지</li>
      <li>구매 내역</li>
      <li>설정</li>
    </MenuBox>
  );
};

export default Modal;

show는 App.tsx에서 Props로 받아와서 상태에 따라
리턴이 어떤값인지 나타낸다.

결과물

이렇게 메뉴버튼 클릭 시 on/off toggle 기능과 그 외에 영역을
클릭 시 메뉴창이 off가 되는 기능을 작업해보았습니다.

profile
한 걸음 한걸음 / 현재는 알고리즘 공부 중!

0개의 댓글