현재 투두앱으로 포트폴리오를 만들고 있는 중이었다. 단순히 리스트를 추가하고, 삭제, 수정만이 하는게 아니라, 노드로 백엔드까지 만들어서 진행하려고 했으나...
생각보다 컴포넌츠는 나누는 작업이 많았고, 회원가입 및 로그인까지 작업을 해야되서 첫 포폴치고는 작업량이 많았다.
그래서 일단은 투두앱
, 회원가입 및 로그인
이 두 가지로 나누기로 하였다. 그 전에 작업에 많이 쓰이는 기술을 먼저 정리하려고 한다.
첫번째로 팝업 모달이다.
코드는 깃헙에서 확인 가능합니다.
팝업 모달은 스위치처럼 on / off 할 수 있는 팝업 창이다.
간단하게는 버튼을 클릭하여 팝업창이 뜨고, X 버튼을 눌러 닫는 것이다. 그 외에 X 버튼 뿐만 아니라, 그 외 영역을 클릭 시에도 팝업창을
닫게 만드는 것이다.
아마 사이트에서는 없어서는 안되는 기능일거다.
한번 정리를 해보자!
이 작업에서는 webpack
typescript
emotion
을 사용하여
작업을 진행하였다.
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
로 진행을 하게 되었다.
mode
- 개발용 또는 배포용 어떤 환경으로 진행할지 정하면 된다.
devtool
- 디버깅 프로세스를 향상 시키는 영역이며, 개발시에는 eval
, 배포용에는 hidden-source-map
을 사용한다.
entry
- 진입할 파일 (SPA는 하나의 진입점만 넣음)
output
- 진입을 하게 된다면, 당연히 결과물도 있겠죠. 그 결과물을 어떻게 내보낼지에 대한 영역입니다. path는 결과물이 있는 디렉토리를 말합니다. filename은 결과물의 파일명이며, 여기서 [name]
은 entry에서 app
을 말합니다. 그래서 실제로 결과물은 app.js가 나오죠.
추후 나머지 부분은 더 정리하도록 하겠습니다.
{
"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": "."
}
}
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "Node",
"target": "es5",
"esModuleInterop": true
}
}
tsconfig 설정 또한 추후 추가적으로 정리하도록 하겠습니다.
<!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 영역은 흔히 보는 패턴입니다. 여기서 script
의 src
를 보면 webpack 설정에서 output
의 경로를 적어주시면 됩니다.
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'));
이 부분 또한 흔히 보는 영역이라, 설명은 없습니다.
이제 팝업 모달에 대해서 정리하겠습니다.
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;
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가 되는 기능을 작업해보았습니다.