Astro 프레임워크 체험기

 

챗 지피티에게 물어본 

Astro의 주요 특징
1. 멀티 프레임워크 지원: Astro는 React, Vue, Svelte, Solid.js 등 다양한 프론트엔드 프레임워크의 컴포넌트를 한 프로젝트에서 함께 사용할 수 있습니다.
2. Partial Hydration (부분 하이드레이션): Astro는 기본적으로 정적 HTML을 생성하며, 필요한 부분만 JavaScript로 하이드레이션(동적으로 활성화)하여 클라이언트에서 실행합니다.
이를 통해 JavaScript 로딩을 최소화하고 페이지 로드 속도를 최적화합니다.
3. 파일 기반 라우팅: 파일 구조를 기반으로 라우팅을 자동으로 처리합니다.
4. 빠른 빌드와 최적화: Vite를 기반으로 하여 빌드 속도가 매우 빠릅니다. 정적 HTML을 우선 생성하고, 필요한 JavaScript만 로드하므로 성능이 뛰어납니다.
5. SEO와 접근성 최적화: 정적 콘텐츠 중심이라 검색 엔진 최적화(SEO)와 초기 로드 속도가 뛰어납니다.
6. Markdown과 통합: Markdown 파일을 쉽게 작성하고 렌더링할 수 있어 블로그나 문서 사이트에 적합합니다.

 

사용 사례
- 블로그나 개인 포트폴리오
- 문서 사이트 (예: 개발 문서, 튜토리얼)
- 마케팅 랜딩 페이지
- SEO가 중요한 정적 웹사이트

 

장점
- 빠른 성능과 작은 JavaScript 번들 크기
- 멀티 프레임워크 호환성
- 쉬운 정적 사이트 생성
- 개발자 경험이 우수함 (간단한 API와 설정)

 

 


 

React, Vue, Svelte, Solidjs 등의 웹 프레임워크의 컴포넌트 지원? 궁금해서 간단히 써봤다. 

Astro 사이트 들어가보면 https://astro.build/ Get Started를 통해 튜토리얼 진행하면 되는 정도의 체험기.


튜토리얼을 진행해보자 

프로젝트 설치

// create a new project with npm
npm create astro@latest

Project initializing

 

dir 프로젝트 위치와 프로젝트명

tmpl 어떤 템플릿 쓸건지 (A basic, minimal starter가 추천이라 그냥했다)

How would you like to start your new project?

deps 종속성 관련 설치하고

git 레포지토리는 연결하지않았다

 

그러면 astro.config.mjs 가 포함된 astro 프로젝트가 생성된다. 

new project

 

타입스크립트 https://docs.astro.build/en/guides/typescript/

 

TypeScript

Learn how to use Astro's built-in TypeScript support.

docs.astro.build

위 문서대로 따라해보자.

 

tsconfig.json 

extends 는 base, strict, strictest 중에 설정할수 있다.

npm install @astrojs/ts-plugin

 

@astrojs/ts-plugin 설치한다음 tsconfig.json에 추가해준다

{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"],
  "compilerOptions": {
    "plugins": [
      {
        "name": "@astrojs/ts-plugin"
      },
    ],
  }
}

 

 

 

자 이제 체험이니까 다른 설정관련해서는 넘어가고 

React 관련 설정을 좀 해보겠다

https://docs.astro.build/en/guides/integrations-guide/react/

 

@astrojs/react

Learn how to use the @astrojs/react framework integration to extend component support in your Astro project.

docs.astro.build

 

Astro에는 astro add를 통해 공식적으로 통합 설정을 자동화하는 명령이 포함되어 있다고한다.

아래처럼 입력하면 react 관련해서 통합 설정해주고, vue로 바꾸면 vue에 대한 통합 설정을 해준다. (오~)

npx astro add react

 

즉, 수동으로 종속 패키지를 install 안하고 통합된 종속 패키지들을 한방에 다운받을수 있다. (오!)

continue? 라고 물어보면 설치 끝날때까지 계속 엔터로 넘기기 

완료되면 어디 뭐 추가됐는지 보여준다. 친절하다.

설치 완료 후 노출창

 

 

설치를 마친후 astro.config.mjs들어와보면, 아래와 같이 react가 추가된 걸 볼수 있다.

// @ts-check
import { defineConfig } from 'astro/config';

import react from '@astrojs/react';

// https://astro.build/config
export default defineConfig({
  integrations: [react()]
});

 

 

vue도 동일하게 진행

https://docs.astro.build/en/guides/integrations-guide/vue/

npx astro add vue

 

 

그리고 스타일에 사용할 tailwind로 똑같이 진행.

https://docs.astro.build/en/guides/integrations-guide/tailwind/

 

똑같은 과정을 거치고 나면 astro.config.mjs 파일에 vue, tailwind가 추가되어있을것이다.

 

최초 화면은 어떻게 생겼을지,

dev를 명령어를 입력하고 띄어보자. 

npm run dev

 

요렇게 기본 화면 뜨는것 까지 확인!

초기화면

 

 

디렉토리 및 파일 (https://docs.astro.build/en/basics/project-structure/)

  • src/*- 프로젝트 소스 코드(구성요소, 페이지, 스타일, 이미지 등)
  • public/*- 코드가 아니고 처리되지 않은 자산(글꼴, 아이콘 등)
  • package.json- 프로젝트 매니페스트.
  • astro.config.mjs- Astro 구성 파일. (권장)
  • tsconfig.json- TypeScript 구성 파일. (권장)

 

react, vue로 컴포넌트

자 이제 react, vue로 컴포넌트를 만들어보자.

 

근데 src > components > Welcome.astro 파일을 보면,,

구조가 신기하다.

 — 주석과 — 주석 사이에 스크립트 넣고 하위는 html, style을 넣어준다. (스타일을 제외한 코드)

 

Welcome.astro

---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---

<div id="container">
	<img id="background" src={background.src} alt="" fetchpriority="high" />
	<main>
		<section id="hero">
			<a href="https://astro.build"
				><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
			>
			<h1>
				To get started, open the <code><pre>src/pages</pre></code> directory in your project.
			</h1>
			<section id="links">
				<a class="button" href="https://docs.astro.build">Read our docs</a>
				<a href="https://astro.build/chat"
					>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
						><path
							fill="currentColor"
							d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
						></path></svg
					>
				</a>
			</section>
		</section>
	</main>

	<a href="https://astro.build/blog/astro-5/" id="news" class="box">
		<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
			><path
				d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
				fill="#111827"></path></svg
		>
		<h2>What's New in Astro 5.0?</h2>
		<p>
			From content layers to server islands, click to learn more about the new features and
			improvements in Astro 5.0
		</p>
	</a>
</div>

<style>
// ...
</style>

 

 

기본적으로 html 렌더링된거 보면 type="module" 로 컴포넌트 코드가 들어가있더라.

 

 

여튼 React와 Vue 컴포넌트를 생성해보자. (각 구조에 맞춰 만들어야함)

components 파일 하위에 컴포넌트를 하나 생성해준다. 

 

 

ReactHeader.tsx

export default function ReactHeader() {
  return <div className="bg-pink-200">ReactHeader</div>;
}

 

 

VueBody.vue

<script>
export default {};
</script>

<template>
  <div className="bg-blue-50">Vue Body</div>
</template>

 

 

astro는 파일기반 라우팅이라 /src/pages 파일 경로에 따라 사이트 엔드포인트가 된다. (에러페이지 같은 경우는 src/pages/404.astro 이런형태로 만들면 된다.

(src/pages에서 파일 유형을 지원: .astro .md .mdx .html .js)

 

pages/index.astro에서 import 해보면 되겠지?

 

index.astro

---
import ReactHeader from '../components/ReactHeader';
import VueBody from '../components/VueBody.vue';
import Layout from '../layouts/Layout.astro';
---

<Layout>
	 <ReactHeader />
	 <VueBody />
</Layout>

 

 

오 문제없이 (vscode가 경고를 안하고있다) import가 되었다.

props는 각각 아래와 같이(문법대로) 넘기면 된다. (느낌아니까)

 

index.astro

---
import ReactHeader from '../components/ReactHeader';
import VueBody from '../components/VueBody.vue';
import Layout from '../layouts/Layout.astro';
---

<Layout>
	 <ReactHeader title="React Header"/>
	 <VueBody title="Vue Body"/>
</Layout>

 

 

 

ReactHeader.tsx

interface Props {
  title: string;
}

export default function ReactHeader(props: Props) {
  return <div className="bg-pink-200">{props.title}</div>;
}

 

 

VueBody.vue

<script>
export default {
  props: {
    title: String,
  },
};
</script>

<template>
  <div className="bg-blue-50">{{ title }}</div>
</template>

 

 

props도 각 구조에 맞게 사용하면된다.

자 이제 클릭을 받는 버튼과, 클릭 시 클릭 횟수를 노출시켜보겠다.

 

ReactHeader.tsx

import { useState } from "react";

interface Props {
  title: string;
}

export default function ReactHeader(props: Props) {
  const [count, setCount] = useState(0);

  return (
    <div className="bg-pink-200">
      <div className="bold">{props.title}</div>
      <div>{`클릭횟수: ${count}`}</div>
      <button
        className="border-[1px] border-blue-500 bg-white px-4 py-1 rounded-full"
        onClick={() => {
          setCount((prev) => prev + 1);
        }}
      >
        클릭
      </button>
    </div>
  );
}

 

 

이때 클릭 눌러도 클릭횟수는 변경되지 않는다. 

 

왜냐면?

기본적으로 Astro는 모든 UI 구성 요소를 HTML 및 CSS로만 자동 렌더링하고 클라이언트 측 JavaScript를 모두 자동으로 제거하기 때문이라고한다. (참고: https://docs.astro.build/en/concepts/islands/)

 

따라서 UI 구성요소에 JavaScript를 사용하고 싶다면,

client:* 지시문을 추가해야한다.

추가할 경우 JavaScript를 자동으로 빌드하고 번들링한다.

 

그리고 상호작용은 구성 요소 수준에서 구성되므로 사용에 따라 각 구성 요소에 대해 다른 로딩 우선순위를 처리할 수 있다고한다.

예를 들어, client:idle 브라우저가 유휴 상태가 되면 구성 요소에 로드할 수 있고 client:visible 뷰포트에 들어가면 한 번만 구성 요소에 로드할 수 있다. 일단 astro 쓰기로했으면 무조건 필수로 알아둬야할 것 같다. 

 

 

index.astro

---
import ReactHeader from '../components/ReactHeader';
import VueBody from '../components/VueBody.vue';
import Layout from '../layouts/Layout.astro';
---

<Layout>
	 <ReactHeader title="React Header" client:load/>
	 <VueBody title="Vue Body"/>
</Layout>

 

아까 그 코드에 client:load를 넣어주기만하면,, 잘 동작한다.

뭐 만들어보고 싶은건 없어서, 진짜 간단하게 astro란 뭔가 를 찾아보면서 체험?만 해봤다.

 

오늘의 일기(?) 끗~~ 

 

구글캘린더 - 팀원 일정 긁어와서 휴가 모아보기 캘린더 만들기

모아보기 캘린더

 

사전작업

모아보기 캘린더 작업할 소유자(나)에게 각 팀원의 캘린더 공유 (모든일정보기)가 되어있어야한다.

(내가 공유받은 팀원의 캘린더로 부터 일정을 긁어오기 때문에)

 

캘린더 공유 방법

구글 캘린더에서 캘린더 우측 (1) 옵션 버튼 > (2) 설정 및 공유 > 설정 페이지에서 (3) 일정의 액세스 권한' 항목 [공개 사용 설정] 에서 소유자에게 공개 > (4) 모아보기(공유) 캘린더 만들기

이때 모두 공개는 보안적으로 좋지 않으므로 소유자에게만 따로 공개하기, 공유 가능한 링크 받기 기능이나 '특정 사용자 또는 그룹과 공유'를 통해 소유자에게 공개해야함

(1) 옵션 버튼
(2) 설정 및 공유

 

3) 일정의 액세스 권한' 항목 [공개 사용 설정] 에서 소유자에게 공개

 

모아보기 캘린더 추가 절차는, 구글 캘린더 화면에서 (1) 다른 캘린더 추가 > (2) 새 캘린더 만들기 > 생성 페이지에서 캘린더 만들기 진행 

(4.1) 다른 캘린더 추가

 

(4.2) 새 캘린더 만들기

 

 

구글의 Apps Script 사용하여 구성원의 일정을 모아보기할것이므로 구글 Apps Script 방문 (https://script.google.com/home) 하여 새 프로젝트 만들기 클릭

새 프로젝트 만들기 진입 시

 

이때 어떠한 정보를 긁어 올 것인지도 단어나 문장을 맞춰야한다.

예를 들면 '휴가' 일 경우, 공유자들의 캘린더 등록 시 타이틀에 '휴가'라는 단어가 필수로 들어가야한다.

공유자의 캘린더에 [휴가] 일정 추가

 

 

Apps Script의 Code.gs에 스크립트를 추가해줘야한다.

일단 돌아가는 상황을 간단히 설명하기 위해, 아래와 같이 콘솔로그를 등록해서 저장을 하면.

아무거나 입력 후 저장

 

저장이 되면서 이렇게 myFunction이 추가되는걸 볼수있다. 함수를 여러개 만들면 눌렀을 경우 리스트로 확인할 수 있다.

myFunction이 노출

 

// 기존 트리거를 삭제하고 새로운 트리거를 설정하는 함수
function createHourlyTrigger() {
  // 현재 프로젝트의 모든 트리거를 가져옴
  const triggers = ScriptApp.getProjectTriggers();

  // 동일한 트리거가 있는지 확인하고, 있으면 삭제
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'myHourlyTask' &&
        trigger.getEventType() === ScriptApp.EventType.CLOCK) {
      ScriptApp.deleteTrigger(trigger);  // 기존 트리거 삭제
      Logger.log('기존 정시 트리거가 삭제되었습니다.');
    }
  });

  // 새로운 트리거 생성
  ScriptApp.newTrigger('myHourlyTask')  // 실행할 함수 이름
    .timeBased()  // 시간 기반 트리거 사용
    .everyHours(1)  // 매시간마다 실행 (정각에 실행)
    .create();     // 트리거 생성
  Logger.log('새로운 정시 트리거가 생성되었습니다.');
}

 

여기에 챗지피티에 질문해서 얻은 스크립트를 넣어보겠다.

간단히 설명하면, ScriptApp.getProjectTriggers를 통해 Code.gs에 작성한 함수들을 가져올 것이고,

가져온 리스트에서 특정 함수 (myHourlyTask)를 1시간 마다 실행하도록 적용할 것이다.

(= 1시간마다 공유자들의 캘린더 정보를 가져와서 원하는 것을 뽑아내서 모아보기 캘린더에 노출)

 

이후 이어서 하단에 아래 스크립트도 추가해준다.

// 매 정시에 실행될 함수
function myHourlyTask() {
  Logger.log('정시에 실행되었습니다.');

  // 원본 캘린더 ID와 소유자 이름 매핑
  const sourceCalendars = { // 구성원 캘린더 ID : 이름
    '삐약@gmail.com': '삐약', 
    // ... 해당 포맷에 맞춰 쭉 작성하기
  };

  // 대상 캘린더 ID
  const destinationCalendarId = ''; 

  // 원하는 날짜 범위 설정
  const today = new Date(); // 현재 날짜
  const startDate = new Date(today.getFullYear(), today.getMonth(), 1); // 이번 달 1일
  const endDate = new Date(today.getFullYear(), today.getMonth() + 6 + 1, 0); // 종료 날짜를 현재 날짜 기준 6개월 이후의 마지막 날로 설정

  const keyword = '휴가'; // 필터링할 일정 제목 키워드
  const copiedTag = '#copied'; // 복사된 일정에 추가할 태그

  // 대상 캘린더에서 기존 이벤트들을 삭제
  const destinationCalendar = CalendarApp.getCalendarById(destinationCalendarId);
  const existingEvents = destinationCalendar.getEvents(startDate, endDate);

  // 기존의 모든 이벤트 삭제
  existingEvents.forEach(event => {
    event.deleteEvent();
  });

  // 모든 원본 캘린더를 순회하며 일정 가져오기
  Object.keys(sourceCalendars).forEach(calendarId => {
    const events = CalendarApp.getCalendarById(calendarId).getEvents(startDate, endDate);
    
    events.forEach(event => {
      if (event.getTitle().includes(keyword)) {
        // 일정 소유자 정보 추가
        const ownerName = sourceCalendars[calendarId];

        // 일정의 시작 시간과 종료 시간을 24시간제로 변환
        const formattedStartTime = Utilities.formatDate(event.getStartTime(), Session.getScriptTimeZone(), 'HH:mm');
        const formattedEndTime = Utilities.formatDate(event.getEndTime(), Session.getScriptTimeZone(), 'HH:mm');

        // 일정이 전날 자정부터 다음날 자정까지 이어지는지 확인
        const startTime = event.getStartTime();
        const endTime = event.getEndTime();
        const isOverMidnight = startTime.getDate() !== endTime.getDate() && startTime.getHours() === 0 && endTime.getHours() === 0;

        if (isOverMidnight) {
          // 전날 자정부터 다음날 자정일 경우, 시작 날짜에 하루종일 일정으로 추가
          const previousDay = new Date(endTime); // 종료 시간에서 하루 전으로 설정
          previousDay.setDate(previousDay.getDate() - 1); // 전날로 설정

          // 전날로 하루 종일 일정 추가 (종료 시간을 제외)
          destinationCalendar.createAllDayEvent(
            `[휴가] - ${ownerName}`, // 시간 값 없이 제목만 표시
            previousDay // 전날로 시작 시간 설정
          );
        } else {
          // 다른 일정의 경우 정상적으로 시작 시간과 종료 시간을 표시
          const newTitle = `[휴가] - ${ownerName} (${formattedStartTime} ~ ${formattedEndTime})`;

          // 대상 캘린더에 이벤트 복사
          destinationCalendar.createEvent(
            newTitle, // 일정 제목에 소유자 이름과 시간 추가
            event.getStartTime(),
            event.getEndTime(),
            {
              location: event.getLocation(),
              description: (event.getDescription() || '') + ' ' + copiedTag, // 복사된 일정에 태그 추가
              guests: event.getGuestList().map(guest => guest.getEmail()).join(','),
              sendInvites: false
            }
          );
        }
      }
    });

 

1시간 마다 실행되는 myHourlyTask의 코드를 간단히 설명해보자면,

- sourceCalendars에 공유자들의 이메일과 모아보기 캘린더에 노출될 이름을 입력한다.

- destinationCalendarId에는 모아보기 캘린더의 id를 입력면된다. 모아보기 캘린더 id는 캘린더 설정 및 공유 페이지에서 (캘린더 공유방법 참고) > '캘린더 통합' 영역의 캘린더 ID를 넣어주면 된다. (example@@group.calendar.google.com) example은 보안적인 특정 유니크 값으로 이루어져있다. 

- keyword에 공유자들 캘린더에서 가져올 키워드를 입력한다. (스크립트에 공백체크가 없으므로 공백까지 맞춰야한다) 

- startDate, endDate에 필요날 날짜를 입력하고, (나는 이번달 1일 ~ 6개월 이후까지 지정) new Date('2025-01-01') 이런식으로 직접 지정해줘도 되고, 필요한 날짜를 계산해서 넣어도된다.

 

아래 노출할 문자열들을 입력한다.

if문 하루종일 인 경우
else 문 시간 휴가인 경우

 

 

완성된 코드

// 기존 트리거를 삭제하고 새로운 트리거를 설정하는 함수
function createHourlyTrigger() {
  // 현재 프로젝트의 모든 트리거를 가져옴
  const triggers = ScriptApp.getProjectTriggers();

  // 동일한 트리거가 있는지 확인하고, 있으면 삭제
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'myHourlyTask' &&
        trigger.getEventType() === ScriptApp.EventType.CLOCK) {
      ScriptApp.deleteTrigger(trigger);  // 기존 트리거 삭제
      Logger.log('기존 정시 트리거가 삭제되었습니다.');
    }
  });

  // 새로운 트리거 생성
  ScriptApp.newTrigger('myHourlyTask')  // 실행할 함수 이름
    .timeBased()  // 시간 기반 트리거 사용
    .everyHours(1)  // 매시간마다 실행 (정각에 실행)
    .create();     // 트리거 생성
  Logger.log('새로운 정시 트리거가 생성되었습니다.');
}

// 매 정시에 실행될 함수
function myHourlyTask() {
  Logger.log('정시에 실행되었습니다.');

  // 원본 캘린더 ID와 소유자 이름 매핑
  const sourceCalendars = { // 구성원 캘린더 ID : 이름
    '삐약@gmail.com': '삐약', 
    // ... 해당 포맷에 맞춰 쭉 작성하기
  };

  // 대상 캘린더 ID
  const destinationCalendarId = ''; 

  // 원하는 날짜 범위 설정
  const today = new Date(); // 현재 날짜
  const startDate = new Date(today.getFullYear(), today.getMonth(), 1); // 이번 달 1일
  const endDate = new Date(today.getFullYear(), today.getMonth() + 6 + 1, 0); // 종료 날짜를 현재 날짜 기준 6개월 이후의 마지막 날로 설정

  const keyword = '휴가'; // 필터링할 일정 제목 키워드
  const copiedTag = '#copied'; // 복사된 일정에 추가할 태그

  // 대상 캘린더에서 기존 이벤트들을 삭제
  const destinationCalendar = CalendarApp.getCalendarById(destinationCalendarId);
  const existingEvents = destinationCalendar.getEvents(startDate, endDate);

  // 기존의 모든 이벤트 삭제
  existingEvents.forEach(event => {
    event.deleteEvent();
  });

  // 모든 원본 캘린더를 순회하며 일정 가져오기
  Object.keys(sourceCalendars).forEach(calendarId => {
    const events = CalendarApp.getCalendarById(calendarId).getEvents(startDate, endDate);
    
    events.forEach(event => {
      if (event.getTitle().includes(keyword)) {
        // 일정 소유자 정보 추가
        const ownerName = sourceCalendars[calendarId];

        // 일정의 시작 시간과 종료 시간을 24시간제로 변환
        const formattedStartTime = Utilities.formatDate(event.getStartTime(), Session.getScriptTimeZone(), 'HH:mm');
        const formattedEndTime = Utilities.formatDate(event.getEndTime(), Session.getScriptTimeZone(), 'HH:mm');

        // 일정이 전날 자정부터 다음날 자정까지 이어지는지 확인
        const startTime = event.getStartTime();
        const endTime = event.getEndTime();
        const isOverMidnight = startTime.getDate() !== endTime.getDate() && startTime.getHours() === 0 && endTime.getHours() === 0;

        if (isOverMidnight) {
          // 전날 자정부터 다음날 자정일 경우, 시작 날짜에 하루종일 일정으로 추가
          const previousDay = new Date(endTime); // 종료 시간에서 하루 전으로 설정
          previousDay.setDate(previousDay.getDate() - 1); // 전날로 설정

          // 전날로 하루 종일 일정 추가 (종료 시간을 제외)
          destinationCalendar.createAllDayEvent(
            `[휴가] - ${ownerName}`, // 시간 값 없이 제목만 표시
            previousDay // 전날로 시작 시간 설정
          );
        } else {
          // 다른 일정의 경우 정상적으로 시작 시간과 종료 시간을 표시
          const newTitle = `[휴가] - ${ownerName} (${formattedStartTime} ~ ${formattedEndTime})`;

          // 대상 캘린더에 이벤트 복사
          destinationCalendar.createEvent(
            newTitle, // 일정 제목에 소유자 이름과 시간 추가
            event.getStartTime(),
            event.getEndTime(),
            {
              location: event.getLocation(),
              description: (event.getDescription() || '') + ' ' + copiedTag, // 복사된 일정에 태그 추가
              guests: event.getGuestList().map(guest => guest.getEmail()).join(','),
              sendInvites: false
            }
          );
        }
      }
    });

 

 

 

작성이 완료되면 다시 저장을 누르고,

createHourlyTrigger (1시간마다 자동실행하는 스크립트) 를 눌러준 다음 왼쪽에 실행 버튼을 눌러준다. 

 

그러면 실행 로그 창이 아래에 열리고 실행되는 절차에 대한 피드백을 주고 (작성했던 스크립트 내의 Logger.log가 노출)

작성했던 스크립트 내의  Logger.log가 노출

 

모아보기 캘린더에서 확인할 수 있다.

결과 

모아보기 캘린더

 

 

이때 참고할 점은 스크립트 도는 도중에는 삭제와 추가하는 과정이 진행 중이기때문에 휴가 확인이 어려울 수 있다.

수동으로 Apps Script에서 [실행]을 누를 경우 매 1시간마다 실행되므로, 실행 시간이 달라지게 된다.

 

코드는 챗지피티가 만들었고 이후 최적화는 안했다.

중요한게 아니어서 최적화할 정도의 필요성을 못느껴서이고, 확실히 코드가 별로다 (삭제-생성이 반복되서 날짜 값이 길면 오래걸리거나 실패할수 있다.) 

기존 작업자가 작업해 둔 코드가 있었고, 요구 조건에 따라 이슈가 전달되어왔다.

 

기존 코드

function addComma(value) {
	return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

 

addComma 결과 값

 

요구 조건은 아래와 같았고.. 

1. 숫자 천 단위마다 콤마 추가

2. 소수점에서는 콤마 추가하지 않아야 함

 

기존 코드에서는 소수점에서도 콤마가 추가되고 있었다.

function addComma(value) {
	return value.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ',');
}

수정한 addComma 결과 값

 

잘되는 듯 했지만, 크로스브라우징 이슈가 있었다. 

safari에서 흰페이지로 뜨는 것이었다. 두둥.. 

그리고 SyntaxError: Invalid regular expression: invalid group specifier name 에러 메시지가 출력되고 있었다.

 

왜???

검토해보니, 정규식에 사용된 lookbehind 패턴 때문이었다.

 

정규식에는 두가지 알고리즘이 존재한다고 한다.

  • Deterministic Finite Automaton (DFA): 문자열의 문자를 한번만 확인한다.
  • Nondeterministic Finite Automaton (NFA): 최적의 일치를 찾을 때 까지 여러번 확인한다.

여기에서 자바스크립트는 NFA 알고리즘을 사용하고 있고 이 알고리즘의 동작으로 인해 Catastrophic Backtracking 가 일어날 수 있다고 한다.

 

regexp-catastrophic-backtracking 란?

더보기

오랜 시간 실행됨에 있어서 js 엔진이 중단되는 현상이라고 한다.

일반적인 증상으로 특정 문자열의 경우 CPU를 100% 사용하여 중단된다고 한다. 

그리고 중단되기 때문에 화면에 아무것또 뜨지 않았던 것이다.(;;)

해결법으로는..

1. 조합의 수를 줄여서 순서대로 찾아 나가는 방식

2. 역 추적을 방지하는 lookahead 패턴으로 개발

두번째 방식을 권장한다고 한다.

 

정규식의 전방 탐색(Lookahead)과 후방 탐색(Lookbehind) 패턴에 대해 간단히 알아보면...

전방 탐색은 작성한 패턴에 일치하는 영역이 존재하여도 그 값이 제외되어서 나오는 패턴이고

후방탐색은 전방 탐색이 앞에 있는 문자열을 탐색하는 거라면 후방탐색은 뒤에 있는 문자열을 탐색하는 것이라고 한다.

이해가 잘 안되면 이 블로그 글을 읽어보자! (저는 이해에 도움이 많이되었습니다 -> https://blog.hexabrain.net/205)

 

즉, 처음에 수정한 정규식에서 (?<!\.\d*) 이 부분이 lookbehind에 해당하는 것이었다.

(1234.1234) <- 뒤의 문자열을 탐색

해결법을 찾기 위해 검색에 검색을 해봤고~!

정리가 잘되어있는 블로그 글을 발견했다!!! ㄳㄳ -> Javascript에서 천 단위 구분 기호로 쉼표가 있는 숫자를 인쇄하는 방법

 

그리고 선택한 방법들을 작성해보았다.!

1. split, join

function addComma(value) {
	let splitVal = value.toString().split('.');   
	splitVal[0] = splitVal[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); 
    return splitVal.join("."); 
}

split-join 방식

 

2. replace function

function addComma(value){ 
	return value
    	.toString()
        .replace(/(\..*)$|(\d)(?=(\d{3})+(?!\d))/g, (digit, fract) => fract || digit + ',');
}

replace function을 방식

 

3. toLocaleString('ko-KR')

이때 maximumFractionDigits를 따로 수정하지 않으면 소수점 자릿수는 3으로 기본 값으로 출력된다.

(예시로 5로 바꾸어봤다)

function addComma(value) {
	return value.toLocaleString('ko-KR', { maximumFractionDigits: 5 });
}

toLocaleString 방식

 

여러 방식을 알아보면서 정규식에 대해서 정말정말 공부하면 도움이 많이 되겠다고 생각한 하루였다.

인프런에 정규식 강의가 있던데,.ㅎㅎ..

 

 


참고!! (감사감사! 합니다.)

toLocaleString()

lookbehind 문법은 사파리와 익스플로러에서 쓸 수 없다.

Javascript에서 천 단위 구분 기호로 쉼표가 있는 숫자를 인쇄하는 방법

내장 메소드를 사용하여 숫자 천단위마다 콤마 찍기

isUndefined와 isEmpty 비교

    console.log('빈 배열', `isUndefined: ${isUndefined([])} / isEmpty: ${isEmpty([])}`);
    console.log('빈 오브젝트', `isUndefined: ${isUndefined({})} / isEmpty: ${isEmpty({})}`);
    console.log('빈 문자열', `isUndefined: ${isUndefined('')} / isEmpty: ${isEmpty('')}`);
    console.log('null', `isUndefined: ${isUndefined(null)} / isEmpty: ${isEmpty(null)}`);
    console.log('undefined', `isUndefined: ${isUndefined(undefined)} / isEmpty: ${isEmpty(undefined)}`);

    console.log('숫자', `isUndefined: ${isUndefined(1234)} / isEmpty: ${isEmpty(1234)}`);
    console.log('문자열', `isUndefined: ${isUndefined('hello')} / isEmpty: ${isEmpty('hello')}`);
    console.log('불린 값: true', `isUndefined: ${isUndefined(true)} / isEmpty: ${isEmpty(true)}`);
    console.log('불린 값: false', `isUndefined: ${isUndefined(false)} / isEmpty: ${isEmpty(false)}`);

 

console.log로 확인 한 값

isUndefined와 isEmpty 비교

 

정리

빈 배열, 오브젝트, 문자열, null 확인: isEmpty

null 확인 : isEmpty (-> ?? (Nullish coalescing operator) 등으로 해결)

undefined 확인: 둘다 값으로 인식 (-> value === undefined 등으로 해결)

숫자 확인: isEmpty는 숫자를 넣으면 비어있다고 인식하는 버그(?)가 있다.

불린 값 확인: isUndefined는 둘다 비어있다고 인식한다. (-> value, !value 등으로 해결)

 


lodash doc: https://lodash.com/docs/4.17.15

+ Recent posts