diff --git a/public/images/cubeit-intranet-retrospect/figjam-plan.avif b/public/images/cubeit-intranet-retrospect/figjam-plan.avif new file mode 100644 index 0000000..7fbf040 Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/figjam-plan.avif differ diff --git a/public/images/cubeit-intranet-retrospect/mediaquery.gif b/public/images/cubeit-intranet-retrospect/mediaquery.gif new file mode 100644 index 0000000..764f81a Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/mediaquery.gif differ diff --git a/public/images/cubeit-intranet-retrospect/member-detail.png b/public/images/cubeit-intranet-retrospect/member-detail.png new file mode 100644 index 0000000..d7c16ce Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/member-detail.png differ diff --git a/public/images/cubeit-intranet-retrospect/mobile-design.avif b/public/images/cubeit-intranet-retrospect/mobile-design.avif new file mode 100644 index 0000000..f517be5 Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/mobile-design.avif differ diff --git a/public/images/cubeit-intranet-retrospect/pc-design.avif b/public/images/cubeit-intranet-retrospect/pc-design.avif new file mode 100644 index 0000000..9927655 Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/pc-design.avif differ diff --git a/public/images/cubeit-intranet-retrospect/profile-edit.avif b/public/images/cubeit-intranet-retrospect/profile-edit.avif new file mode 100644 index 0000000..8b6b24b Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/profile-edit.avif differ diff --git a/public/images/cubeit-intranet-retrospect/profile-page-component.avif b/public/images/cubeit-intranet-retrospect/profile-page-component.avif new file mode 100644 index 0000000..90d1769 Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/profile-page-component.avif differ diff --git a/public/images/cubeit-intranet-retrospect/profile-page.png b/public/images/cubeit-intranet-retrospect/profile-page.png new file mode 100644 index 0000000..842873b Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/profile-page.png differ diff --git a/public/images/cubeit-intranet-retrospect/random-profile-image.gif b/public/images/cubeit-intranet-retrospect/random-profile-image.gif new file mode 100644 index 0000000..8ef1f52 Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/random-profile-image.gif differ diff --git a/public/images/cubeit-intranet-retrospect/teamwork.avif b/public/images/cubeit-intranet-retrospect/teamwork.avif new file mode 100644 index 0000000..0ff39c4 Binary files /dev/null and b/public/images/cubeit-intranet-retrospect/teamwork.avif differ diff --git a/src/content/post/blog/cuebit-intranet-retrospect.mdx b/src/content/post/blog/cuebit-intranet-retrospect.mdx new file mode 100644 index 0000000..ef3d7c6 --- /dev/null +++ b/src/content/post/blog/cuebit-intranet-retrospect.mdx @@ -0,0 +1,214 @@ +--- +title: '바닐라 자바스크립트로 SPA 애플리케이션 만들기' +description: '바닐라 자바스크립트로 CubeIT 인트라넷 서비스를 만들어보자.' +date: '2024-07-18' +tags: + - 김민태의 데브캠프 + - 토이프로젝트 + - 프론트엔드 프로젝트 +--- + +김민태의 데브캠프의 첫 번째 토이 프로젝트로 '**관리자용과 사용자용으로 구분된 인트라넷 서비스 설계 및 개발**'이라는 미션이 주어졌다. + +**바닐라 자바스크립트로 SPA**를 구현해야 했는데 라이브러리나 프레임워크의 도움 없이 기본적인 구조부터 데이터베이스, 라우팅 등 모든 걸 직접 구현해야 해서 처음에는 너무나도 막막했다. +하지만 팀원들과 회의를 진행하며 차근차근 하나씩 의견을 나누다 보니 조금씩 방향성을 잡아갈 수 있었다. + +## 팀 소개 + +우리 조의 팀명은 `(mbt)i-dle` 이다. 팀원 모두 MBTI가 I로 시작해서 팀명을 `아이들`로 지었다. +인트라넷 서비스의 회사 이름은 `Cube.IT(큐브잇)`으로 정했는데 (아이돌) 아이들의 소속사인 Cube와 IT 회사라는 의미의 IT를 합친 이름이다. 😁 + +## 팀 목표 + +- 다양한 예외 사항을 고려해 **필수 기능**을 제대로 구현하기 +- **Git과 Github** 잘 활용해 보기 +- 사용자 친화적인 **UI/UX** 설계하기 +- **보안** 강화하기 + +우리는 **필수 기능을 제대로 구현**하는 걸 최우선 과제로 삼았다. 대부분 팀 프로젝트 경험이 없거나 적었기 때문에 깃과 깃허브를 잘 활용해 효율적인 협업 방식을 배워보는 것도 중요한 목표였다. +서비스 목표로는 실제 사용자를 고려한 UI/UX 설계와 인트라넷 서비스인 만큼 여러 개인 정보를 포함하고 있어 보안에도 신경 써보자는 목표를 정했다. + +개인적인 목표는 **익숙한 것보다는 새로운 도전 해보기**와 **이유 있는 선택하기**였다. + +## 기술 스택 + +`Vite` 기반으로 만들어진 템플릿이 주어졌다. 요구사항에 맞게 프론트엔드 파트는 `HTML, CSS, JavaScript`를 사용해 구현했다. + +템플릿에 `Express` 기반의 간단한 서버가 구현되어 있었고 `SQLite3` 패키지가 설치되어 있었다. +SQLite3는 선택사항이었지만, 김민태 강사님께서 SQL 문을 사용해 데이터베이스를 다뤄보면 좋은 경험이 될 거라고 하셔서 도전해 보기로 결정했다. + +사용한 주요 라이브러리는 다음과 같다. + +- `Axios`: API 통신을 위해 사용 +- `Bcrypt`: 비밀번호 암호화에 사용 +- `Cloudinary`: 사용자가 업로드한 이미지를 url로 변환해 저장하기 위해 사용 +- `Dayjs`: 현재 시각 표시 및 캘린더 구현 등에 사용 +- `JsonWebToken(JWT)`: 클라이언트와 서버 간 보안정보를 안전하게 전달하기 위해 사용 + +## 기획 + +![Figjam 기획 일부](/images/cubeit-intranet-retrospect/figjam-plan.avif) + +주어진 필수 요구 사항을 바탕으로 다 같이 기획을 시작했다. 프로젝트 경험이 있으신 조장님이 **아이스브레이킹, 페르소나 설정, 와이어프레임 설계** 등 기획을 이끌어주셔서 수월하게 진행됐다. +피그잼은 처음 사용해 봤는데 기획에 활용하기 좋아서 앞으로도 사용할 계획이다. + +기획을 바탕으로 기능 정의서, 컴포넌트 관계도 등 문서 작업을 진행했다. + +![컴포넌트 관계도 일부 - Profile 페이지](/images/cubeit-intranet-retrospect/profile-page-component.avif) + +초반에 컴포넌트 관계도는 문서에서 제외하자는 이야기가 나왔는데, 설계하는 데 도움이 될 것 같아 내가 만들겠다고 했다. +팀원들이 컴포넌트 관계도 덕분에 **전반적인 구조를 파악하기 좋았다**고 해주셔서 뿌듯했다! + +## 디자인 + +김민태 강사님께서 프론트엔드 개발자에게 **디자인 감각**은 중요한 요소이니 디자인 리서치를 해보며 UI 요소들을 직접 만들어보는 경험을 가지면 좋을 것 같다고 하셨다. +사실 팀에 디자인할 줄 아시는 분이 두 분이나 계셔서 내가 하게 될 줄은 몰랐는데 어쩌다 보니 디자인을 맡아서 하게 됐다.ㅎㅎ + +![Figma PC 디자인](/images/cubeit-intranet-retrospect/pc-design.avif) + +![Figma 모바일 디자인](/images/cubeit-intranet-retrospect/mobile-design.avif) + +반응형으로 구현 예정이었기 때문에 **PC와 모바일 두 가지 디자인**을 완성했다. 오랜만에 해서 그런지 디자인 작업은 나름 재미있게 끝낸 것 같다. +디자인이 나와야 개발을 시작할 수 있어서 레퍼런스를 활용해 주말에 빠르게 작업했다. +조장님(진짜 디자이너)이 레퍼런스도 구해주시고 피드백도 빠르게 주셔서 좀 더 수월하게 작업할 수 있었다. + +![반응형으로 구현한 모습](/images/cubeit-intranet-retrospect/mediaquery.gif) + +## 역할 + +디자인 외에도 프로젝트 세팅, 기본 구조 및 라우팅 설계, 프로필 페이지 등 다양한 역할을 맡았다. + +### 프로젝트 세팅 + +`깃허브 관리자`라는 역할을 맡아 프로젝트 세팅을 담당했다. **코딩 컨벤션**을 정하고 **이슈 템플릿, PR 템플릿** 등도 알아봤다. +다양한 깃허브 프로젝트 저장소를 찾아보며 **Github Wiki**를 활용해 보면 좋겠다는 생각이 들어 제안하기도 했다. + +### 기본 구조 및 라우팅 설계 + +`Class` 문법을 사용해 컴포넌트 단위로 개발했다. +각 컴포넌트는 `html` 메소드로 템플릿을 설정하고, `render`와 `addEventListeners` 등의 메소드를 활용해 일관된 구조를 유지할 수 있도록 설계했다. + +라우팅은 `History API`를 활용했다. Hash 방식도 고려했지만 URL에 `#`이 포함되어 가독성이 떨어지고 보통 같은 페이지 내의 요소를 가리키는데 사용되는 방식이라 좀 더 **깔끔한 URL 구조**를 만들 수 있는 `History API` 방식을 선택했다. + +SPA 구현을 위해 `popstate` 이벤트와 `document.body`의 클릭 이벤트로 페이지 전환을 처리했다. 링크 클릭 시 `handleNavigatePage` 메소드가 실행되어 페이지가 전환된다. + +URL의 **동적 파라미터**를 처리하기 위해 정규식을 사용한 `matchRoute` 함수도 구현했다. + +```js +// /members/:id 같은 동적 경로 처리 +export const matchRoute = (path, routes) => { + if (!routes) { + return null + } + + let matchedRoute = null + + Object.keys(routes).forEach((routePath) => { + if (matchedRoute) return + + const route = routes[routePath] + const paramNames = [] + const regexPath = routePath.replace(/:[^\s/]+/g, (match) => { + paramNames.push(match.slice(1)) + return '([^\\/]+)' + }) + + const regex = new RegExp(`^${regexPath}$`) + const match = path.match(regex) + if (match) { + matchedRoute = route + } + }) + + return matchedRoute +} +``` + +### 프로필 페이지 + +Cube.IT 인트라넷 서비스는 크게 **로그인, 홈, 프로필, 구성원, 근무/휴가 페이지**로 구성되어 있다. 그중 내가 맡은 페이지는 `프로필 페이지`였다. + +![프로필 페이지 소개](/images/cubeit-intranet-retrospect/profile-page.png) + +프로필 페이지에서는 **근무 시작/종료** 및 **본인 프로필 수정**을 할 수 있다. +재사용할 수 있게 컴포넌트 단위로 만든 덕분에 구성원 상세 페이지에서도 해당 컴포넌트를 사용해 빠르게 구현할 수 있었다. + +![프로필 수정](/images/cubeit-intranet-retrospect/profile-edit.avif) + +이미지 파일을 업로드하면 **Cloudinary**를 이용해 **url로 변환**한 다음, **데이터베이스에 저장**한다. +그대로 데이터베이스에 저장할 수도 있었지만, 용량 최적화를 위해 Cloudinary 서비스를 이용했다. + +![랜덤 프로필 이미지 생성](/images/cubeit-intranet-retrospect/random-profile-image.gif) + +`기본 이미지 설정`을 클릭하면 [DiceBear](https://www.dicebear.com/) API의 Lorelei 스타일 **아바타를 랜덤으로 설정**해주는 이스터에그 같은 기능을 추가했다. +처음 만들었을 때 재미있어서 홀린 듯이 계속 눌러봤던 기억이 있다. 🤣 + +### 구성원 상세 페이지 + +![권한에 따라 확인할 수 있는 정보가 다름](/images/cubeit-intranet-retrospect/member-detail.png) + +위에서 언급한 것처럼 프로필 페이지에서 만든 컴포넌트를 조합해 구현했다. + +구성원 상세 페이지에서는 권한에 따라 확인할 수 있는 정보가 다르다. +`관리자`는 연봉, 주소, 고용 정보, 학력 및 경력 등 **구성원의 모든 정보를 확인**할 수 있고, **수정**도 할 수 있다. +반면 `일반 직원`은 다른 구성원의 **제한된 정보**만 확인할 수 있다. + +### 더미 데이터 생성 + +`ChatGPT`를 활용해 직원, 부서, 휴가 관리 등 여러 테이블의 연관 데이터를 생성했다. +실제 서비스처럼 보이게 하려고 더미 데이터 생성에 많은 공을 들였는데, 그 시간을 전체적인 구조 설계에 더 투자했다면 좋았을 것 같다는 아쉬움이 든다. + +## 프로젝트를 마치며 + +데이터를 어떻게 구성할지, 어떤 UI/UX가 더 사용하기 편할지, 어떻게 하면 안전하게 로그인한 사용자 정보를 저장해둘 수 있을지, 어느 시점에 어떻게 렌더링할지 등 정말 많은 요소를 고민하고 답을 찾아가며 프로젝트를 진행했다. + +많은 고민이 있었던 만큼 좋은 결과물이 나올 수 있었던 것 같고, 혼자 했으면 절대로 이 정도 퀄리티를 낼 수 없었을 것 같다. +팀원들이 각기 다른 강점을 가지고 있어서 서로에게 배울 점이 많았는데 이번 프로젝트 경험이 다음 프로젝트에 큰 도움이 될 거라는 생각이 든다. + +## 좋았던 점 + +### 팀 분위기 + +![매니저님의 코멘트](/images/cubeit-intranet-retrospect/teamwork.avif) + +매니저님도 인정하실 정도로 팀 분위기가 정말 좋아서 프로젝트를 진행하는 내내 즐거웠다. +슬랙 소통도 활발했고 (덕분에 짤 폴더도 생겼다) 다들 맡은 일을 열심히 해줬다. + +### 이슈 및 PR 활용 + +이슈에 구현해야 할 기능들을 미리 추가해 두고, 구현할 사람이 해당 이슈에 본인을 할당하는 방식으로 프로젝트를 진행했다. +이슈 목록을 보면 **누가 어떤 작업을 하고 있는지 쉽게 파악**할 수 있어서 프로젝트 진행에 큰 도움이 됐다. + +PR은 **2명 이상 승인**했을 때 머지할 수 있도록 설정했다. **코드 리뷰 문화**를 도입해 수정이 필요한 부분이나 개선했으면 하는 부분에 대한 리뷰를 남기면 반영했고 덕분에 프로젝트의 퀄리티가 높아질 수 있었다. + +중반쯤 **마일스톤** 기능에 대해 알게 돼서 마일스톤도 추가했는데, 프로젝트가 얼마나 진행됐는지 파악하기 좋아서 유용했다. + +### 목표 달성! + +프로젝트가 끝나고 돌아보니 팀에서 목표로 한 부분들을 대부분 이룬 것 같아 신기하고 뿌듯했다. +개인적인 목표도 나름대로 달성한 것 같다. 우선 api 설계, 데이터베이스, Cloudinary 연동 등 백엔드 코드에 도전해 볼 수 있었고, 바닐라 자바스크립트로 SPA를 만들기 위한 구조를 설계하고 클래스 문법으로 구현해 보는 등 낯설지만 도전 자체만으로도 의미 있는 경험을 할 수 있었다. + +배포도 도전해 봤는데 다음 프로젝트에서 또 배포를 하게 된다면 이번 경험을 바탕으로 좀 더 수월하게 할 수 있을 것 같다. + +기능을 추가하거나 라이브러리 등을 정할 때도 항상 근거를 들어 의견을 제시하려고 노력했다. 다른 팀원분들도 각자 근거를 들어 의견을 제시해 주셔서 큰 갈등 없이 프로젝트를 진행할 수 있었다. + +## 아쉬웠던 점 + +### 깃허브 프로젝트 활용 + +깃허브의 기능을 최대한 활용해 보고 싶어서 추가했는데, 이번 프로젝트에서는 실효성이 없었다. +이슈를 만들 때 프로젝트에 연결하긴 했지만, 프로젝트 페이지에 들어가 확인하는 팀원도 없었고 그냥 연결할 일 하나만 더 추가된 느낌이라 아쉬웠다. + +## 문서화 + +문서화를 하지 않은 건 아닌데 자료를 찾는 데 어려움이 있었다. +회의록을 날짜로 관리하고 있어서 일정표를 찾기 위해 여러 회의록을 열어봐야 하는 등 (6/26 회의록 안에 있었다...) 원하는 자료를 빠르게 찾기 어려웠다. + +제출을 위한 문서가 아닌 프로젝트 진행에 도움이 되는 문서 정리를 하기 위한 방법을 고민해 봐야 할 것 같다. + +## 맺으면서 + +처음엔 정말 막막했는데 모두 힘을 합쳐 좋은 결과물을 만들어낼 수 있어서 뿌듯하고 즐거운 시간이었다. 우리 조도 정말 잘했지만, 다른 조도 각자의 개성을 담은 멋진 결과물을 만들어내셔서 발표를 보면서 대단하다는 생각이 들었다. +열심히 한 만큼 후회는 없고 이번 프로젝트를 통해 배운 것들을 다음 프로젝트에서 잘 활용해 보고 싶다. + +CubeIT의 전체 코드는 [Github](https://github.com/Dev-FE-1/idle-intranet-service)에서 확인할 수 있다.