Vanilla JS로 구현한 캘린더 웹앱, 피플(Piece Plan)

💛 nalsae·2023년 5월 12일
1

💭 프로젝트 회고

목록 보기
4/7
post-thumbnail

 지난 번 HTML, CSS만을 활용한 프로젝트에 이어 이번에는 JavaScript까지 활용했던 프로젝트를 회고해보려고 한다. 한 가지 특이점이 있다면, 프로젝트 전에 JavaScript만으로 React와 같은 SPA-CBD 기반 라이브러리, 프레임워크의 Reconciliation 알고리즘을 유사하게 구현해보는 경험을 했었다. 이 경험을 토대로 Vanilla JS로 Single Page 렌더링을 구현하고, 상태 값이 바뀔 때마다 변경된 부분만 선택적으로 리렌더링되도록 하는 것이 이번 프로젝트의 가장 큰 취지였다.

 이번에는 프로젝트 진행 기간이 2주 정도 되었기 때문에 기존 웹 사이트를 개선하기 보다는 직접 간단하게라도 기획부터 디자인, 개발까지 경험해보기로 했다. 사실 2주도 기획, 디자인, 개발까지 완벽하게 소화하기에는 너무 짧은 시간이지만 팀원 중 한 명이 이미 아이디어를 구체화해놓은 상태였기 때문에 기획에 큰 시간을 들이지 않을 수 있었다. 그 덕에 빠르게 디자인 시안을 제작하고 개발에 좀 더 많은 시간을 투자하여 나름 만족스러운 결과물을 도출했다.

 역시 개인 Notion과 GitHub Wiki에 이미 기록해놓은 내용이 있지만, 날 것의 내용을 좀 더 다듬어서 이번 글을 작성해보려고 한다. 지금부터 본격적으로 시작!


📌 프로젝트 요약

  • 진행 인원
    : 4명

  • 진행 기간
    : 2022.10.09 ~ 2022.10.21(약 2주)

  • 기획 의도
    : 사람들이 공유한 효율적인 루틴을 모아서 자신만의 독창적인 일정을 계획할 수 있는 캘린더 웹 애플리케이션을 제작

  • 기술 스택

  • 프로젝트 특징
    : Component Based Development 방식
    : Single Page Rendering으로 동작
    : Vanilla JS로 CBD-SPA Library의 Diffing 알고리즘을 유사하게 구현 및 프로젝트에 적용

📑 기획 단계

 먼저 기획 단계에서는 전체적인 프로젝트의 주제, 작업 일정, 디렉토리 구조와 컨벤션에 대해 논의하고, 와이어프레임과 플로우 차트를 작성했다. 이를 요약하면 다음과 같다.

  • 프로젝트 주제
    : 모두가 자신의 루틴을 공유하고 그 루틴을 바탕으로 새롭게 일정을 생성할 수 있는 애플리케이션, 피플(Piece-Plan)

  • 작업 일정


  • 와이어프레임 제작
    : 카카오 오븐을 활용한 와이어프레임 image

  • 플로우차트 제작
    : 피그마를 활용한 플로우 차트 image

  • 역할 분담
    : 기본적으로 페이지 혹은 컴포넌트 단위로 구분하고, 각 페이지의 담당자를 정한다. 담당자는 담당하는 페이지의 HTML, CSS, JS 작업을 총괄한다.

    🔸 작업 단위 목록
    (1) 메인 페이지
    (2) 로그인 / 회원가입 페이지
    (3) 전체 캘린더 페이지
    (4) 일별 캘린더 페이지
    (5) 상세 정보 모달
    (6) 피스 추가 모달
    👉 (4), (5)을 내가 담당하기로 결정

  • 컨벤션 논의
    : 작업과 관련하여 Prettier, ES Lint, CSS, JS, Git 등의 컨벤션 논의사항

🎨 디자인 단계

 기획 단계에서 제작한 와이어프레임을 토대로 콘셉트를 정한 뒤 Figma로 간단한 디자인 시스템과 디자인 시안 제작에 착수했다. 디자인 시안을 제작할 때마다 느끼는 부분이지만, 개발 지식의 유무가 디자인 시안을 접하는 개발자의 편의성에 큰 영향을 미치는 것 같다. 이번에도 직접 제작한 디자인 시안이 있어서 든든했다고 팀원들이 얘기해줘서 정말 뿌듯했다. 제작한 디자인 시안은 다음과 같다.

🔸 디자인 시스템

🔸 메인 페이지

🔸 로그인 / 회원가입 페이지

🔸 전체 캘린더 페이지

🔸 피스 편집 페이지

🔸 피스 등록 모달


🔨 개발 단계

🔸 스키마 구축

 개발 단계에서는 먼저 백엔드 경험이 있는 팀원을 주축으로 데이터 스키마를 구축했고, 간단한 데이터 스키마는 다음과 같다.

🔸 기술 검토

 또한 프로젝트에 사용할 Router, 서버, JWT 관련 기술 검토를 선행적으로 진행했다. Vanilla JS에서 Routing이나 인증, 인가를 처음 구현해보는 거였기 때문에 기능 구현 이전에 팀원들 모두 각자 기술 검토를 하고, 이를 공유하면서 스터디하는 시간을 가졌다. 이 덕분에 낯선 기술 스택을 쉽게 받아들일 수 있었던 것 같다.

🔸 컴포넌트별 HTML & CSS 작업

 기술 검토가 얼추 마무리된 후에는 각자 분담한 역할에 따라 컴포넌트별로 HTML과 CSS 작업을 먼저 완료했다. 물론 우리의 프로젝트는 Vanilla JS로 CBD-SPA Library처럼 동작하게끔 만드는 것이 목적이었기 때문에 JS를 이용하여 HTML을 동적으로 렌더링할 것이다. 하지만 그 전에 HTML 템플릿을 먼저 제작할 필요가 있다고 판단하여 각자 작업에 착수했다.

🔸 컴포넌트별 JS 작업

 HTML과 CSS 작업이 끝난 후에는 각자 작업한 HTML, CSS를 클래스로 컴포넌트화하는 작업을 이어서 담당했다. 여기서 내가 담당한 컴포넌트는 일별 캘린더 모달과 피스의 상세 정보를 볼 수 있는 모달이었다.

🔸 Routing 구현

 어느 정도 컴포넌트 구현을 마무리한 후에는 컴포넌트끼리 연결을 위해 Routing을 구현했는데, Routing 로직을 코드로 간단하게 설명하면 다음과 같다.

// route별로 렌더링할 컴포넌트
const routes = [
  { path: '/', component: Main },
  { path: '/login', component: Login },
  { path: '/signup', component: Signup },
  { path: '/calendar', component: Calendar, guard: true },
  { path: '/plan', component: Plan, guard: true },
];

// route를 매개변수로 받아서 해당하는 route에 맞는 컴포넌트 생성
const render = async path => {
  // 매개변수가 없으면 현재 url의 path 전달
  let _path = path ?? window.location.pathname;
  if (_path.lastIndexOf('/') !== 0) {
    _path = _path.substring(0, _path.lastIndexOf('/'));
  }

const route = routes.find(route => route.path === _path);

// 렌더링할 컴포넌트가 존재하지 않으면 오류 페이지 렌더링
let CurrentComponent = route?.component || NotFound;
...}

🔥 렌더링 이슈 발생과 해결

 팀원들이 각자 담당한 컴포넌트를 작업한 후에는 서로 상하위 관계에 있는 컴포넌트를 결합하는 과정을 거쳤다. 그런데 그 과정 중에 자잘한 렌더링 이슈가 발생하기 시작했고, 결론부터 이야기하자면 this 바인딩이 원인이었다. 자세한 원인 분석과 해결 과정은 따로 작성한 아래의 게시글에 적어두었다.

🔨 클래스 컴포넌트에서 발생하는 렌더링 이슈 부수기

: https://velog.io/@nalsae/Vanilla-JS%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%9C-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%9D%B4%EC%8A%88

🔸 서버 구현

 컴포넌트 작업이 거의 마무리가 되었을 시점부터는 서버 구현에 힘을 쏟았다. 이전에 팀원들과 함께 검토했던 express.js 모듈을 사용하여 구현해보았는데, 처음 사용해보는 모듈이라 처음에는 어떻게 구현해야 할지 난감했다. 팀원들과 함께 머리를 싸매고 결과적으로는 서버 구현도 성공했다. 서버 로직은 다음과 같다.

// npm 설치 후 import 해온 모듈 및 미들웨어
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');

// 환경 변수 파일 연결
require('dotenv').config();

// 서버 객체 생성 및 PORT 연결
const app = express();
const PORT = process.env.PORT || 5500;

// 루트 경로 연결
app.use(express.static('public'));
app.use('/plan', express.static('public'));

🔥 Routing 이슈 발생과 해결

 그런데 잘 구현된 줄만 알았던 서버 로직에서도 문제가 발생했다. 전반적으로 문제가 생긴 건 아니고 Route가 2번 이상 중첩되는 일별 캘린더 페이지의 렌더링이 제대로 동작하지 않았다. /login, /signup/로 구분되는 Route가 하나밖에 없는 페이지와 다르게 일별 캘린더 페이지는 날짜 구분을 위해 /plan/20221022처럼 Route가 중첩되는 형식으로 Routing을 구현했었다. 그래서 이 경우에는 Routing이 정상적으로 동작하지 않았다. 이는 express.js 모듈의 static 미들웨어를 사용하여 해결할 수 있었는데, 구체적인 해결 과정은 아래의 게시글에서 볼 수 있다.

🔨 express.static 미들웨어를 이용한 중첩 Routing 구현

: https://velog.io/@nalsae/express.static-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A4%91%EC%B2%A9-Routing-%EA%B5%AC%ED%98%84

🔸 인증 & 인가 구현

 우리 프로젝트에서는 인증과 인가를 구현할 때 Session 대신 JWT를 선택했다. 서버와 DB가 거의 없다고 무방할 뿐만 아니라, 서비스 규모 자체도 작았기 때문이다. 인증과 인가, Session과 JWT에 대한 기술 검토 정리 내용은 아래의 게시글에서 확인할 수 있다.

🔑 인증과 인가, Session과 JWT

: https://velog.io/@nalsae/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-Session%EA%B3%BC-JWT

 아무튼 기술 검토를 토대로 구현한 인증, 인가 로직을 간단하게 코드로 첨부하자면 다음과 같다.

// 클라이언트
if (this.getValid()) {
  const { userid: id, password } = this.state.values;
  // HTTP POST 요청의 응답으로 불리언 값(로그인 성공 여부)을 전달 받아 저장
  const { data: isSuccess } = await axios.post('/login', { id, password }, { withCredentials: true });
     
  // isSuccess 값이 false면, 즉 로그인이 실패하면 에러 처리 후 리턴
  if (!isSuccess) {
    this.setState({ isLoginError: true });

    const timerId = setTimeout(() => {
      alert('아이디 또는 비밀번호를 확인해주세요.');

      this.setState({ isLoginError: false });
      clearTimeout(timerId);
    }, 300);

    return;
  }

  // isSuccess 값이 true면, 즉 로그인이 성공하면 캘린더 페이지 렌더링
  this.changePage('/calendar');
} else {
  this.setState({ isLoginError: true });

  const timerId = setTimeout(() => {
    alert('아이디 또는 비밀번호를 확인해주세요.');
    this.setState({ isLoginError: false });

    clearTimeout(timerId);
  }, 300);
}
  
// 서버
app.post('/login', (req, res) => {
  const { id, password } = req.body;

  const user = users.find(user => user.id === id && user.password === password);

  // DB 중 일치하는 정보가 없으면 false를 HTTP 응답으로 전달
  if (!user) {
    res.send(false);
    return;
  }

  // DB 중 일치하는 정보가 있으면 JWT 발급 후 HTTP 응답으로 true 전달
  const accessToken = jwt.sign({ userId: user.userId, name: user.name }, process.env.JWT_SECRET_KEY, {
    expiresIn: '1d',
  });

  res.setHeader('Access-Control-Allow-Credentials', 'true');

  res.cookie('accessToken', accessToken, {
    maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
    httpOnly: true,
  });

  res.send(true);
});

🔥 쿠키 관련 CORS 이슈 발생과 해결

 하지만 상단의 구현 과정이 마냥 순탄한 것은 아니었다. 쿠키에 JWT를 동봉하여 주고 받는 과정에서 아무리 쿠키를 참조하고 뜯어 보아도 JWT를 확인조차 할 수 없는 것이다. 그런데 JWT를 발급하는 서버 단에서 console로 확인해보면 발급된 토큰이 정상적으로 출력되었다. 미치고 팔짝 뛸 노릇이었다. 대체 뭐가 문제인지 몇 시간 동안 씨름하고.. 구글링도 해보고.. 결국 문제를 성공적으로 해결할 수 있었다. 그 과정은 아래의 게시글에 따로 작성해보았다.

🔨 쿠키, CORS 이슈와의 눈물 나는 씨름

: https://velog.io/@nalsae/%EC%BF%A0%ED%82%A4-CORS-%EC%9D%B4%EC%8A%88%EC%99%80%EC%9D%98-%EB%88%88%EB%AC%BC-%EB%82%98%EB%8A%94-%EC%94%A8%EB%A6%84

 거의 다 왔다! 인증과 인가 구현까지 완료한 후에는 자잘한 오류를 수정하고 다양한 플랫폼으로 배포를 시도할 차례다. 배포 과정에 대해 소개하기 전에 마주쳤던 진짜 어이없고 웃긴 이슈가 하나 있어서 소개해보려고 한다.

🔥 회원가입 시 닉네임 관련 이슈 발생과 해결

 문제는 바로 회원가입 시 사용자가 닉네임을 입력하는 과정에서 발생했다. 사용자가 닉네임을 입력할 때 <h2>닉네임</h2>과 같은 방식으로 HTML 태그처럼 닉네임을 입력했을 때 정말 어이없는 상황이 발생했는데, 이를 어떻게 해결했는지는 아래 게시글에 간단하게 작성해보았다.

😂 빵터지는 Sanitization 관련 이슈 해결

: https://velog.io/@nalsae/%EB%B9%B5%ED%84%B0%EC%A7%80%EB%8A%94-Sanitization-%EA%B4%80%EB%A0%A8-%EC%9D%B4%EC%8A%88%EC%99%80-%ED%95%B4%EA%B2%B0

🔸 배포하기

 배포도 엄청 순탄치만은 않았는데, 배포 툴을 거의 처음 써보았기 때문에 러닝 커브가 있는 데 비해 프로젝트 마감까지 기한이 얼마 남지 않았다는 점이 마음을 조급하게 만들었다. 배포는 총 3단계의 시도 끝에 최종적으로 CGP(Google Cloud Platform)을 이용하여 성공적으로 마쳤다. 이를 소개하면 다음과 같다.

🔹 Heroku를 활용한 시도

  • 과정

    : Heroku를 활용하여 배포를 시도하였고, 결국 성공했지만 Heroku의 서버가 해외에 있다는 특징 때문에 최초 렌더링 속도와 페이지 이동 시의 렌더링 속도가 굉장히 느리다는 이슈가 발생했다.
    : 결과적으로 Heroku 외에 다른 배포 플랫폼을 사용하여 배포하기로 협의했다.

  • 결과

    : ⭕ 성공 ⭕

🔹 Netlify를 활용한 시도

  • 과정

    : Netlify를 활용하여 배포를 시도하였고, 관련 자료를 구글링해보았으나 대부분 React와 Next.js를 활용한 프로젝트인 경우에 적용할 수 있는 방법들이었다.
    : 추측하건대 서버를 켠 후 추가적인 작업을 진행해야 정상적인 배포가 진행되는 것 같지만, 현재 프로젝트에서는 express 모듈을 사용한 임시 서버만 로직만 존재하고 실제 서버가 존재하지 않는 상태이기 때문에 Netlify를 사용하여 배포를 시도할 때 서버를 켠 이후에 추가적인 배포 작업 진행이 정상적으로 이루어지지 않았던 것 같다.

  • 결과

: ❌ 실패 ❌

🔹 Google Cloud Platform을 활용한 시도

  • 과정

: Netlify를 활용한 배포 시도가 실패한 시점에서 AWS나 GCP를 활용하여 배포를 시도하기로 결정했다.
: 처음에는 AWS를 사용해보려고 했으나 프로젝트 마감까지 남아 있는 시간에 비해 러닝 커브가 매우 높다고 판단하여 결국 GCP를 이용해보기로 결정했다.
: 관련 링크를 참고하여 성공적으로 배포를 마무리할 수 있었고, 결과적으로 기존 Heroku를 활용한 배포보다 훨씬 빠른 렌더링 속도를 도출할 수 있었다.
: 그러나 로컬에 존재하는 프로젝트 폴더를 기준으로 배포가 완료되었기 때문에 GitHub와 GCP를 연동하는 방법을 찾아보고 시도해보았으나 실패, GitHub와 연동하여 push가 이루어질 때마다 CI/CD를 자동화할 수 있도록 후속 조치가 필요할 듯하다.

  • 결과

: ⭕ 성공 ⭕

  • 참고 링크

: https://codingapple.com/unit/nodejs-deployment-googlecloud/


🎉 프로젝트 결과

 이렇게 우리 팀은 배포까지 2주간의 대장정을 성공적으로 마치고 구석구석 살펴보면 리팩토링할 부분이 많겠지만, 처음 기획했던 기능 대부분을 구현하는 데 성공했다. 지금부터는 기획, 디자인 등 앞서 소개한 내용은 제외하고 발표 자료로 사용했던 PPT 슬라이드와 함께 프로젝트 결과를 요약해보고자 한다.

🔸 기술 스택

 프로젝트에 사용한 기술 스택은 위와 같다.

🔸 프로젝트 구조

 프로젝트의 전반적인 디렉토리 구조는 위와 같다. models에는 사용자의 데이터와 데이터를 조작하는 메서드들이, public에는 클라이언트 단의 로직들이, server는 서버 단의 로직들이 포함되어 있다.

 public 폴더는 다시 리소스와 관련된 assets, React의 Reconciliation 알고리즘 구현 로직과 관련된 libs, 원형 컴포넌트인 core, 원형 컴포넌트를 토대로 생성한 자식 컴포넌트와 관련된 components로 구분된다.

🔸 데이터 스키마

 백엔드를 경험해 본 팀원의 도움을 받아 간략하게 구성해본 데이터 스키마와 실제 데이터, 데이터 관련 메서드 구조는 위와 같다.

🔸 REST API

 REST 규칙을 준수한 서버 단의 HTTP 응답 메서드들은 위와 같다.

🔸 Routing

 Route에 따라 페이지 컴포넌트와 그 하위 컴포넌트들을 렌더링하는 방식은 위와 같다.

🔸 구현 이미지

 원래는 배포 페이지 링크에 정상적으로 접속이 가능했지만, CGP 무료 요금제가 끝나는 바람에 현재 접속이 불가능하다 ㅠㅜ.. 그래서 마지막으로 웹 사이트 구현 후 GitHub README 파일에 첨부할 용도로 캡처했던 gif 이미지들을 보여드리고 프로젝트 회고를 마무리하려고 한다.

🔹 회원 가입 기능

🔹 로그인 기능

🔹 전체 캘린더 기능

🔹 피스 추가 및 삭제 기능

🔹 피스 검색 및 필터 기능


💕 좋았던 점

 일단 가장 좋았단 점을 꼽자면 프로젝트를 Vanilla JS로 진행했기 때문에 프로젝트를 마무리할 때쯤이면 스스로 JavaScript 숙련도가 많이 향상되었음을 체감할 수 있었다. 특히 거의 사용해 볼 일이 없었던 클래스로 컴포넌트를 생성한다든가, Vanilla JS로 React의 Reconciliation 알고리즘을 유사하게 구현해보았던 부분이 많은 공부가 되었다. 그리고 그 과정에서 자연스럽게 모던 프론트엔드 개발에서 왜 CBD-SPA 기반의 라이브러리를 범용적으로 채택하여 사용하고 있는지 몸소 체감하게 되었다. 구체적으로 앞서 소개했던 this 바인딩으로 인해 발생했던 이슈들의 원인을 찾고 해결하면서 여실히 느낄 수 있었다. 마지막으로 친숙하지 않았던 express.js의 공식 문서를 정독하고 다뤄보면서 공식 문서를 읽는 것에 대한 거부감을 없앨 수 있었고, 간단하게나마 서버 로직을 구현할 수 있게 된 것 역시 이번 프로젝트를 통해 스스로 성장한 부분이라고 생각한다.

💔 아쉬운 점

 사실 프로젝트에 대한 전체적인 만족도는 높지만, 기한 문제로 추가적으로 구현하고 싶은 기능을 구현하지 못했던 것이 아쉬움으로 남는다. 카카오 API를 사용한 지도, 공유 기능, 유저들끼리의 커뮤니티 기능 등을 추가해보아도 좋을 것 같다는 생각이 든다. 또한 시간만 충분했다면 AWS를 사용하여 배포를 진행하고 싶었는데, 시간에 쫓겨 배포를 휘뚜루마뚜루 하게 된 것도 아쉬운 점인 것 같다. 나중에 개인 프로젝트를 진행할 때는 AWS로 차근차근 배포를 시도해볼 계획이다.

profile
𝙸'𝚖 𝚊 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚝𝚛𝚢𝚒𝚗𝚐 𝚝𝚘 𝚜𝚝𝚞𝚍𝚢 𝚊𝚕𝚠𝚊𝚢𝚜. 🤔

1개의 댓글

comment-user-thumbnail
2023년 6월 23일

와우~~감탄밖에...

답글 달기