본문으로 건너뛰기

· 약 5분
yeheum Choi

new Date()로 달력만들기!

회사에서 달력이 필요한 부분이 있어서 라이브러리 들을 찾아 보았다. 괜찮은 오픈 소스들도 많이 있었지만 스타일을 커스텀하거나 원하는 기능들을 자유롭게 사용하기에는 조금 불편해서 만들어 보기로 했다.

대강 어떻게 만들어야 할지는 생각하면 나올 것 같았지만(?) 편하게 하기위해ㅎㅎ 다른 분들도 잘 해 놓았을 것 같아서 구글링을 해보았다.

여러가지들이 있었지만 특정 라이브러리를 사용해서 하는 경우가 많았다. 그래서 그냥 new Date() 를 사용해서 혼자 만들어 보기로 했다.

1. 스타일 모티브

기본 스타일은 그냥 react-calendar 라이브러리 스타일을 따라서 해보았다.

2. new Date()를 사용해 월별 달력 뽑기

여러가지 방법이 있겠지만 그냥 가장 단순하게 생각나는 방법으로 했다. (수정이 필요할 수도 있을듯)

export function getNewDateObj(newDate: Date) {
const year = newDate.getFullYear();
const month = newDate.getMonth() + 1;
const date = newDate.getDate();
const day = newDate.getDay();
const hours = newDate.getHours();
const minutes = newDate.getMinutes();
const seconds = newDate.getSeconds();

return { year, month, date, day, hours, minutes, seconds };
}

먼저는 편의를 위해 new Date()로 만들어진 것을 파라미터로 받아서 년,월,일 등등으로 리턴하는 함수를 만들어준다.

function getMonthDate(newDate, page = 0) {
const doMonth = getNewDateObj(
new Date(newDate.year, newDate.month - 1 + page, 1)
);

const prevMonthLastDate = getNewDateObj(
new Date(doMonth.year, doMonth.month - 1, 0)
);
const startDate =
prevMonthLastDate.day === 0
? prevMonthLastDate
: prevMonthLastDate.day === 6
? doMonth
: getNewDateObj(
new Date(doMonth.year, doMonth.month - 1, -prevMonthLastDate.day)
);
let monthDate = [];
for (let i = 0; i < 42; i++) {
monthDate.push(
getNewDateObj(
new Date(startDate.year, startDate.month - 1, startDate.date + i)
)
);
}

const week1 = monthDate.slice(0, 7);
const week2 = monthDate.slice(7, 14);
const week3 = monthDate.slice(14, 21);
const week4 = monthDate.slice(21, 28);
const week5 = monthDate.slice(28, 35);
const week6 = monthDate.slice(35);

const week4LastDate = week4[week4.length - 1];
const week5LastDate = week5[week5.length - 1];
const lastDate = new Date(doMonth.year, doMonth.month, 0);
const isLastWeek4 =
week4LastDate.month !== doMonth.month ||
!(week4LastDate.date < lastDate.getDate());
const isLastWeek5 =
week5LastDate.month !== doMonth.month ||
!(week5LastDate.date < lastDate.getDate());
const dateArr = [week1, week2, week3, week4];

return {
year: doMonth.year,
month: doMonth.month,
date: isLastWeek4
? dateArr
: isLastWeek5
? [...dateArr, week5]
: [...dateArr, week5, week6],
};
}
  • 먼저 이 함수는 선택된 날짜 ( 시작 날짜 ) 를 받아서 해당 월의 첫 날을 뽑아낸다. => page는 다음이나 이전 월을 넣을 수 있게 만들었다.
  • new Date()의 세번째 파라미터에 0을 넣어서 지난달의 마지막 날을 뽑아낸다.
  • 지난달의 마지막날이 일요일인지, 토요일인지를 통해서 선택된 날짜의 월 달력의 첫주차의 첫 번째 날짜를 지정해 준다.
  • 아무리 많아봤자 42개의 날짜 이기에 반복문을 통해서 최대 6주차를 뽑아낸다.
  • 해당 월의 마지막 주차의 마지막 날의 월이 start Date의 월과 다른지 비교를 통해서 4,5,6주차를 정해준다. => 4주차 밖에 없을 수도 있다. 2월의 1일이 처음이고 28일이 마지막이라면?

이렇게 해서 완성된 달력이다.

생각보다 간단한 것 같다. 기본적으로 저렇게 선택한 날짜를 state에 저장해서 사용할 수 있도록만 해놓았다. 아마 공휴일이나 지난 날짜를 선택못하게 하는 것은 금방 할 수 있을 것 같다.

· 약 12분
yeheum Choi

로고부터 마음에 드는 Next.js를 알아보도록 하자!

* 서버사이드 렌더링? (SSR)

서버사이드 렌더링이란 서버에서 리액트 코드를 실행해서 렌더링하는 것을 말한다.

그렇다면 서버사이드 렌더링이 왜 필요한 것인가?

  • 검색 엔진 최적화 (SEO)를 도와준다.
  • 빠른 첫 페이지 렌더링을 도와준다.

요즘 주변을 보면 굉장히 많은 스타트업들이 생겨나고 또 보여진다. 그렇다면 각 회사들이 살아남으려면 그만큼 사용자들에게 잘 노출되어야 한다.

구글을 제외한 다른 검색 엔진에서는 자바스크립트를 실행하지 않기 때문에 클라이언트 렌더링만 하는 사이트는 내용이 없는 사이트와 동일하게 처리된다 => 구글도 SSR 사이트에 더 높은 점수를 부여함.

자 그러면 Next.js를 사용해 보기 전에 먼저 직접 서버사이드 렌더링 환경을 구축해 보자.

# 서버사이드 렌더링 초급

  • 리액트에서 제공하는 renderToString, hydrate 함수를 사용해 본다.
  • 서버에서 생성된 데이터를 클라이언트로 전달하는 방법을 알아본다.
  • styled-component로 작성된 스타일이 서버사이드 렌더링 시 어떻게 처리되는지 알아본다.
  • 서버용 번들 파일을 만드는 방법을 알아본다.
mkdir test-ssr
cd test-ssr
npm init -y
npm install react react-dom
npm install @babel/core @babel/preset-env @babel/preset-react
npm install webpack webpack-cli babel-loader clean-webpack-plugin html-webpack-plugin

1. 클라이언트에서만 렌더링해 보기

프로젝트 루트에 src폴더를 만들고 그 밑에 Home.js, About.js 파일을 만든다. 각 파일은 웹사이트의 페이지를 나타내며 페이지 전환을 테스트하는 용도로 사용된다.

// Home.js
import React from "react";

export default function Home() {
return (
<div>
<h3>This is home page</h3>
</div>
);
}

// About.js
import React from "react";

export default function Ablut() {
return (
<div>
<h3>This is about page</h3>
</div>
);
}

이 Home.js와 About.js를 렌더링하는 App 컴포넌트를 만들어 보자. App 컴포넌트는 버튼을 통해 각 페이지로 이동할 수 있는 기능을 제공한다.

import React, { useState, useEffect } from "react";
import Home from "./Home";
import About from "./About";

export default function App({ page }) {
const [page, setPage] = useState(page);

useEffect(() => {
window.onpopstate = (e) => {
setPage(e.state);
};
}, []);

function onChangePage(e) {
const newPage = e.target.dataset.page;
window.history.pushState(newPage, "", `/${newPage}`);
setPage(newPage);
}
const PageComponent = page === "home" ? Home : About;

return (
<div className='container'>
<button data-page='home' onClick={onChangePage}>
Home
</button>
<button data-page='about' onClick={onChangePage}>
About
</button>
<PageComponent />
</div>
);
}

onpopstate 는 뭘까? * onpopstate(MDN)

즉 popstate를 통해서 서버사이드 렌더링시 두 개의 페이지 간에 state를 복사해주기 때문에 state 관리가 가능한 것 같다!

src 폴더 밑에 index.js 파일을 만들고 앞에서 만든 App 컴포넌트를 렌더링해 보자.

import React from "react";
import ReactDom from "react-dom";
import App from "./App";

ReactDom.render(<App page='home' />, document.getElementById("root"));

이제 웹팩을 설정해 보자! (나는 웹팩이 제일 어려운 것 같다)

프로젝트 루트에 webpack.config.js 파일을 만들고 다음 코드를 입력한다.

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
entry: "./src/index.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.js$/,
use: "babel-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./template/index.html",
}),
],
mode: "production",
};

이제 template 폴더를 만들고 그 밑에 index.html 파일을 만든다.

<!DOCTYPE html>
<html>
<head>
<title>test-ssr</title>
</head>
<body>
<div id="root" />
</body>
</html>

바벨 설정하기

자바스크립트 파일을 컴파일 하기 위해 바벨 설정 파일을 작성해 보자. 루트에 babellnofig.js 파일을 만들자

const presets = ["@babel/preset-react", "@babel/preset-env"];
const plugins = [];
module.exports = { presets, plugins };

babel.config.js 파일의 설정은 babel-loader가 실행될 때 적용된다.

클라이언트 렌더링 확인하기 !! 이제 웹펙을 실행해 보자. npx webpack

이건 안되고..

이건 된다!

url이 file://로 시작하기 때문에 push State 메서드를 호출할 때 에러가 발생하기 때문이다. 이는 서버를 직접 띄우는 방식을 이용하면 해결된다.

당연하게도 첫 요청에 대한 응답으로 돌아오는 HTML 에는 버튼이나 문구를 표현하는 돔 요소가 없다. 버튼이나 문구의 돔 요소는 자바스크립트가 실행되면서 추가된다. 만약 브라우저 옵션에서 자바스크립트 실행을 허용하지 않고 실행해 보면 화면에는 아무것도 보이지 않는 것을 확인할 수 있다.

2. 서버사이드 렌더링 함수 사용해 보기

서버사이드 렌더링 함수 네 개

  • renderToString : 정적 페이지를 렌더링할 때 사용
  • renderToNodeStream : 상동
  • renderToStaticMarkup : 최초 렌더링 이후에도 계속해서 상태 변화에 따라 화면을 갱신해야 할 때 사용
  • renderToStaticNodeStream : 상동

먼저 서버사이드 렌더링에 필요한 패키지를 설치해 보자.

npm install express @babel/cli @babel/plugin-transform-modules-commonjs

웹 서버를 띄우기 위해 express 패키지를 설치하고, 서버에서 사용될 자바스크립트 파일을 컴파일 할 때 사용하기 위해 @babel/cli 패키지를 설치한다. => 서버에서도 JSX 문법으로 작성된 자바스크립트를 실행해야 하므로

ESM으로 작성된 모듈 시스템을 commonJS로 변경하기 위해서 그 뒤에 패키지를 설치했다.

웹 서버 코드 작성하기

src 폴더 밑에 server.js 파일을 만들고 다음 코드를 입력하자.

import express from "express";
import fs from "fs";
import path from "path";
import { renderToString } from "react-dom/server";
import React from "react";
import App from "./App";

const app = express();
const html = fs.readFileSync(
path.resolve(__dirname, "../dist/index.html"),
"utf8"
);
app.use("/dist", express.static("dist"));
app.get("/favicon.ico", (req, res) => res.sendStatus(204));
app.get("*", (req, res) => {
const renderString = renderToString(<App page='home' />);
const result = html.replace(
'<div id="root"></div>',
`<div id="root">${renderString}</div>`
);
res.send(result);
});

app.listen(3000);

책을 보면서 작성할 뿐이니 모르는 부분들이 많다. 정리해보도록 하자.

  • react-dom 패키지의 server 폴더 밑에 서버에서 사용되는 기능이 모여 있다.
  • express 객체인 app 변수를 이용해서 미들웨어와 url 경로 설정을 할 수 있따.
  • renderToString 함수를 사용해서 App 컴포넌트를 렌더링한다. renderToString 함수는 문자열을 반환한다.

바벨 설정하기

서버와 클라이언트에서 필요한 바벨 플러그인과 프리셋은 다음과 같다.

  • 클라이언트
    • 바벨 프리셋: @babel/preset-react, @babel/preset-env
    • 바벨 플러그인: 없음
  • 서버
    • 바벨 프리셋: @babel/preset-react
    • 바벨 플러그인: @babel/plugin-transform-modules-commonjs

이제 프로젝트 루트에 .bablerc.common.js, .babelrc.server.js, .babelrc.client.js 파일을 만들고 다음 코들르 입력한다.

// .babelrc.common.js
const presets = ["@babel/preset-react"];
const plugins = [];
module.exports = { presets, plugins };

// .babelrc.client.js
const config = require("./.babelrc.common.js");
config.presets.push("@babel/preset-env");
module.exports = config;

// .babelrc.server.js
const config = require("./.babelrc.common.js");
config.plugins.push("@babel/plugin-transform-modules-commonjs");
module.exports = config;

공통으로 사용되는 설정은 .babel.common.js 에서 관리하고 클라이언트와 서버측 에서는 이 설정을 가져와서 사용한다.

웹팩 설정하기

웹팩 설정 파일에서는 HTML에 추가되는 번들 파일의 경로와 바벨 설정 파일의 경로를 수정해야 한다.

퍼블릭패스와 옵션을 추가했다.

  • publicPath 설정은 html-webpack-plugin이 HTML 생성 시 HTML 내부 리소스 파일의 경로를 만들 때 사용된다. publicPath 설정 없이 생성된 HTML 파일은 브라우저에서 바로 실행하면 문제가 없지만 서버사이드 렌더링을 할 때는 문제가 된다.

기타 설정 및 프로그램 실행하기

서버 측 코드는 @babel/cli를 이용해서 바벨만 실행하고, 클라이언트 측 코드는 웹팩을 실행한다. package.json을 수정해보자.

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build-server": "babel src --out-dir dist-server --config-file ./.babelrc.server.js",
"build": "npm run build-server && webpack",
"start": "node dist-server/server.js"
},

서버사이드 렌더링을 하면 이미 돔 요소가 만들어진 상태이기 때문에 클라이언트 측에서 또다시 렌더링할 필요가 없다. 단, 각 돔 요소에 필요한 이벤트 처리 함수를 연결해야 한다. 이벤트 처리함수를 연결하지 않으면 화면은 잘 보이지만 사용자가 버튼을 눌러도 반응하지 않는다. 리액트에서 제공하는 hydrate 함수는 서버사이드 렌더링의 결과로 만들어진 돔 요소에 필요한 이벤트 처리 함수를 붙여 준다.

index.js 파일에서 hydrate 함수를 사용하도록 추가해보자.

ReactDom.hydrate(<App page='home' />, document.getElementById("root"));

이제 다음과 같이 실행해 보자.

npm run build npm start

그리고 브라우저에서 로컬호스트 3000으로 접속해 보면 화면이 제대로 렌더링되고 페이지를 전환하는 버튼도 잘 동작하는 것을 확인 할 수 있다.

내용이 좀 길어서 서버 데이터를 클라이언트로 전달하는 부분부터 다음 블로깅에 쓰려고 한다.