2022년 처음 GDSC KNU 2기 멤버로 활동하게 된 이후로부터 3기 코어를 맡았고 3기 리드님의 추천으로 4기 리드를 맡게 되었다. 현재 리크루팅 중이고 멤버들을 열심히 모집하고 있다!
https://github.com/GDSC-KNU/GDSC_Official_FE
현재 운영하고 있는 GDSC KNU 서비스이다. 사실 지금 지원되는 기능이야 지원하기 기능밖에 없지만 올해 3월부터 기획을 시작으로 학기 중 바쁘더라도 짬짬이 시간내서 만든 서비스이기에 애증이 남는 서비스이다. 기획은 현재 9개월로 잡고 있어 12월을 기점으로 총 서비스를 마무리할려고 기획 중이다.
GDSC KNU 공식 3기 Core Member로 활동하게 되면서 항상 의문점이 들었던 적이 있다.
회의는 항상 노션에서 진행했었고, 지원폼이나 설문조사는 항상 구글 드라이브로 모든 자료들을 받았으며 음성 관련 화상 회의이나 지세미나는 디스코드에서 진행했었고, 날짜는 항상 구글 캘린더로 회의를 잡았다. 이러다보니 필요한 기능들이 파편화가 되어 회의나 이런 운영관련 활동을 할때 마다 항상 불편하다는 생각이 들었다. 이 모든 것들을 함께 모아놓은 자체 서비스에서 진행을 해보면 좋지 않을까? 라는 생각이 들었고
3월에 차기 4기 운영진을 하고 싶어하는 애들이나 이런 서비스에 대한 욕구가 있는 애들을 모아 팀을 구성하였다.
처음 시작은 프론트엔드 1명(나 혼자), 백엔드 2명, 디자이너 2명, QA 및 회의록 정리담당 1명이었다.
노션 기록들
우선적으로 노션에서 많이 정리하면서 기능 관련 명세서 내용을 열심히 정리하고 설계면에서도 엄청 힘들게 구현을 했던 것 같다.
기획 단계에서 과연 사용자들이 필요가 있을까???라는 내용으로 위에서 불편함을 느꼈던 기능에 대해 추려나가기 시작했고 구현을 해야겠다라는 기능들을 다 정리를 하게 되었다.
우선적으로 디스코드는 WebRTC와 WebSocket을 기능을 사용한 실시간 화상회의 소통 서비스인데 과연 우리가 서버를 감당할 돈이 될까?라는 점에서부터 의문점이 제기되었었다. 또한 관련 트래픽을 경험을 해보지 못하였었고 실제로 운영될 서비스를 감안하였을때 디스코드 만한 퍼포먼스를 낼 수 없다고 판단했기에 디스코드는 따로 냅두었다.
디스코드를 제외하고 지원폼이나 설문조사 같은 경우 서비스는 충분히 자체 서비스내에서 기획할 수 있다고 판단했고 GDSC KNU 같은 경우 항상 지원 마지막 날에 사람 몰리는 것을 감안했을 때 충분히 좋은 경험을 할 수 있다고 판단했기에 구글 드라이브에 자료가 들어가게 되는 것들은 전부 서비스에 담당하기로 하였다.
회의록이나 달력 같은 경우에 초기에 있었던 의문점은 동시편집을 그러면 어떻게 구현하지? 였었다. 이 부분에 관련되어 처음엔 동시편집을 포기하는 방향으로 이 기능들을 포기할까 싶었지만 Yorkie나 Y.js같이 서버에서 부담이 가지않는 자체 라이브러리를 가지고 있었기에 이에 판단하여 회의나 일정을 간단하게 잡을 수 있는 서비스를 넣어야 겠다라고 판단하여 구현하기로 했다.
또한 멤버가 최대한 성장을 이끌어내게 할 수 있는 방법이 무엇이 있을까에 대해서 고민을 해보았었다. 본인 경험 상 개발 후 항상 회고록이나 개발 글을 올렸을 때 좀 힘들었던 점, 억울했던 점, 배웠던 점을 허심탄회하게 글을 썼었기에 이것이 도움된다고 생각했었고 팀원들과 판단했을 때 처음 프로젝트를 경험해보시는 분들이라면 나중에 Tistory나 Velog로 글을 이관한다고 치더라도 처음 글을 쓰는 경험을 만들어 주는 것은 나쁘지 않겠다라고 판단하였고, SEO를 통해 쓴 글들이 GDSC KNU Member들 뿐만 아닌 다른 사람들에게도 노출 시켜주는 경험을 해보는 것도 좋겠다라고 판단하여 개발 블로그도 기능을 넣게 되었다.
사실 코드에 대한 욕심도 있었고 최대한 개발 코드를 이쁘게 치고 싶다라는 의욕이 매우 강했기에 프론트엔드 개발자는 혼자 자처한다고 개발을 착수하였다. 그러나 수많은 기능들 앞에서는 코드 장사없다라는 점을 깨닫게 되었다. 왜 서비스가 커지면 커질수록 더 많은 사람들을 뽑을려고 하는지도 점차 이해가 되더라.
그래서 디자이너 하시는 분 중 한 명은 프론트엔드 개발을 하는 분이셨고 도움을 요청해 같이 개발하기로 진행을 하였다.
우선적으로 현재 멤버들을 뽑기 위한 지원서 및 사람들을 관리하기 위한 어드민 페이지를 우선적으로 제작을 하기로 하였다. 어드민 페이지 같은 경우에는 지원 서류 관리, 멤버들 status 관리, 멤버들을 뽑았을때 팀 배치 관리를 개발을 진행하기로 하였다.
사실 정말 힘들었다. 3월부터 시작하였지만 학업 기간도 걸치면서 같이 개발을 해야되다보니 결국 밀리고 밀려 8월초까지 밀리게 되었었는데 개발을 진행하면서 실제로 운영해야되고 지속적으로 유지보수가 가능한 서비스를 만들어야되다보니 막상 쉽게 코드가 손에 입력이 안 됐었다. 과연 내가 짜는 코드가 맞을까? 성능적으로 괜찮을까? 라는 고민을 항상 진행하다보니 결국 돌아오는 방향은 똑같더라도 코드 속도가 예전만큼 올라오지 않았던 것 같다.
그러나 개발 도중 방학 기간에 카카오 테크 캠퍼스를 활동하면서 강사님이랑 멘토님과 커피챗을 진행했던 적이 있다.
커피챗을 하러 가기 위해 직접 서울로 올라가 강사님 회사 근처에 가서 직접 회사에서 어떤 것을 하는지와 회사에서 추구하는 가치, 내가 고민했던 내용에 대한 답변을 들었다.
이와 관련해서 결국 개발에 목 메이기보다는 어떻게 하면 서비스를 조금 더 사용자에게 빠르게 서비스를 제공할 수 있을까? 어떻게 하면 사용자가 초기에 서비스를 이용하고 지속적으로 찾을 수 있는 서비스를 만들 수 있을까?에 대한 의문점을 가지고 접근을 하면 좋겠다라는 깨달음을 얻을 수 있었다.
이전에 애증이 있는 GDSC KNU 관련해서 조금 더 멋진 서비스를 만들고 싶었기에 코드적으로 더 완벽한 서비스를 만들어야겠다는 압박감을 벗어나게한 계기였다.
그런 점에서 벗어나게 되다보니 코드 속도도 더 빠르게 오르게 되었고 조금 더 사용자에게 더 편하고 사용자가 발생할 수 있는 문제를 제일 최소화하여 서비스를 운영할 수 있을까에 대한 고민을 하다보니 조금 더 실력적으로 상승하게 될 수 있는 계기였던 것 같다.
디자이너 분께서 Carousel을 디자인해주셨는데 이와 비슷한 Carousel라이브러리를 직접 찾지 못했었고 직접 구현하기로 했었다.
이 부분에서 각도 계산을 하기 위해 엄청나게 머리를 싸맸던 것 같다. 제일 고민했던 내용은 카드 carousel안에 지구를 넣는 issue였는데
carousel안에서 지구를 넣는 부분이 기술적으로 불가능하다는 생각이 들었다.
카드별로 z-index를 다 적용해보았지만 지구가 카드 carousel 위에 위치하게 되거나 아예 지구가 carousel 뒤로 숨어버리는 문제가 있었다.
이와 관련해서 카드별로 opacity를 따로 적용해보면 어떨까라는 또 다른 해결책으로 진행을 해보았고 위 영상처럼 지구가 카드 carousel 사이에 위치하는 것처럼 보이지만 사실 뒤에 카드를 투명도를 높여 카드 carousel 뒤에 지구가 위치 시켰다.
이 부분에 관련하여 디자이너 분과 1차 개발물 리뷰때 디자이너분이 이 점이 오히려 더 입체적인 것 같다고 좋게 봐주셨던 것 같다!
이렇게 하룻밤을 꼬박 새면서 개발 후 직접 구현된 carousel을 통해 매우 뿌듯함을 느꼈었다.
사실 이 부분에 관련해서 제일 신경썼었고 Backend 팀원 분도 이와 관련해서 제일 스트레스를 받았던 부분이다. 항상 학교내에서 사람들이 구현했다고 서비스하는 기능들의 썰을 들으면 항상 어디가 뚫렸다더라~ 이런 기능에 허점이 있더라~라는 썰을 많이 들었기 때문에 기본적인 허점이 뚫리지 않는 서비스를 만들고 싶었다.
이와 관련해서 팀원분과 예외 useCase관련해서 회의를 진행하면서 최대한 기능을 막기 위해 개발을 진행하였다.
개발적으로 고민했던 내용
1. 최종 제출을 했을 시에 조회가 되어야 할까?
2. 본인이 아닌 다른 사람들이 본인의 학번을 통해 조회를 할려고하면 어떻게 대처해야 될까?
3. 만약 다른 사람들이 본인의 학번을 통해 조회를 할려고 하게 되었을 때 최대한 개인정보를 적게 노출하면서 지원서 폼을 제출하게 할 수 있는 내용들을 어떤 것들을 넣을 수 있을까?
4. 마감일이 되었을때 지원서폼을 지원 못 하게 할 수 있는 방법은 무엇이 있을까?
이런 점들을 고려하면서 개발을 최대한 진행을 하려고 하였고 서버, 클라이언트 딴에 두 곳에 전부 검증을 진행하여 최대한 본인의 개인정보를 남들에게 노출되지 않도록 노력하였다.
에러가 발생하게 되면 실시간으로 대비하기 위해 현재 운영을 하고 있는 디스코드 채널에 운영진만 볼 수 있는 홈페이지 에러채널을 따로 파서 진행을 하였고
나름 우려했던 내용들 중 하나가 터지게 되면서 서버 측에서 에러 코드를 제대로 남겨주었다. 나름 성공적으로 막을 수 있었기 때문에 사용자들을 제대로 지켜냈구나라는 뿌듯함이 느껴지는 경험을 할 수 있었다!
이 부분을 관련해서 위에서 깨달음을 얻은 상태에서 나 혼자서는 개발을 하는데 서비스시작기간에 못 맞추겠다는 판단을 하였고 팀원 1명과 같이 프론트엔드 개발을 같이 하였다. 표관련해서는 팀원 분께 맡겼었고
나머지 부분 관련해서는 팀배치 기능은 본인이 구현하게 되었다.
팀 배치 기능 관련해서는 이름을 입력해서 넣기보다는 운영진들이 추후 편하게 배치를 하기 위해 드래그앤드랍 기능을 활용하여 배치하게 했으면 좋겠다라고 판단했기 때문에 드래그앤드랍 기능을 사용하여 기능을 구현하였다!
Drag and Drop(드래그앤드랍)
https://github.com/atlassian/react-beautiful-dnd
react-beautiful-dnd 공식문서에 존재하는 gif 이미지이다.
DragDropContext - 응용 프로그램에서 드래그 앤 드롭을 사용하도록 설정할 부분을 랩합니다
Dropable - 드롭할 수 있는 영역.
Draggable - 끌고 다닐 수 있는 것
라이브러리를 선택했는 이유
드래그앤드랍을 구현하기 위해서는 기본적으로 이해를 해야되는 내용들이 많다.
우선 HTML에서 요소가 드래그 이벤트가 발생할 수 있도록 해당 요소의 속성으로 draggable을 통해서 값을 줄 수 있는 걸로 알고 있는데
- dragstart: 사용자가 드래그를 시작하려고 할 때 발생함.
- drag: 대상 객체를 드래그하면서 마우스를 움직일 때 발생함.
- dragenter: 마우스가 대상 위로 처음 진입할 때 발생함.
- dragover: 드래그하면서 마우스가 대상 객체의 영역 위레 자리 잡고 있을 때 발생함.
- drop: 드래그가 끝나서 드래그하던 객체를 놓는 장소에 위치한 객체에서 발생한다. 드래그된 데이터를 가져와서 드롭 위치에 넣는 역할을 한다.
- drogleave: 드래그가 끝나서 마우스가 대상 위를 벗어날 때 발생함.
- dragend: 대상 객체를 드래그하다가 마우스 버튼을 놓는 순간 발생한다.
이런 이벤트를 전부 꿰뚫고 있어야 쓰기 편하다.
또한 데이터 전송 기능 같은 경우에도 수많은 기능이 필요하기에
react-beautiful-dnd라는 기능을 사용하게 되었다.
react-beautiful-dnd 기능 중단??!?!
전부 구현 완료 후 콘솔창으로 이동했는데 지원 중단 관련 경고창이 떠서 깃헙을 들어가보니 지원을 중단한다는 이야기가 있었다. 앞으로 버전 관련해서 추후 유지보수를 생각했을때 라이브러리 이동이 필요하다는 생각이 들었고
https://github.com/hello-pangea/dnd
16버전까지 나온 현재 대체되고 있는 라이브러리로 넘어가게 되었다. 다행히 쓰는 방식은 비슷했기에 코드 자체적으로 수정해야될 일은 거의 없었다!
나름 부드럽게 잘 넘어오는 것을 볼 수 있다!
앞으로 지속적인 서비스를 만든다고 생각을 하고 개발을 진행하게 되다보니까 어떻게하면 사용자에게 서비스를 지속적으로 노출시킬 수 있을까하는 고민이 들었었다.
Next.js를 사용하면 RSC를 통해 html 자체를 분리하여 배포되기 때문에 SEO 고민을 할 필요가 없지만 React 자체 같은 경우 SPA라는 특성때문에 index.html파일을 하나만 robot한테 색인을 요청하기때문에 상대적으로 어렵게 된다.
SEO를 올리기 위한 방안
이 부분에 대해서 고민을 하던 도중 react-async-helmet이라는 기능에 대해서 고민을 하게 되었다.
react-async-helmet는 서버 사이드 렌더링(SSR)을 지원하는 React 애플리케이션에서 HTML head
태그 내의 요소를 동적으로 관리하고 제어하는 데 사용되는 라이브러리이다. react-async-helmet는 react-helmet의 확장된 버전으로, 비동기 데이터 로딩과 같은 시나리오에서 HTML 헤드 관리를 지원한다.
본인은 OG 그래프와 Twitter 카드 쪽 부분까지 고려를 해서 SEO 컴포넌트를 작성하였고
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title: string;
description: string;
url: string;
image: string;
}
export const SEO = ({ title, description, url, image }: SEOProps) => {
return (
<Helmet>
<title>{title}</title>
<meta name='description' content={description} />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:image' content={image} />
<meta property='og:url' content={url} />
<meta property='og:locale' content='ko_KR' />
<meta property='og:image:width' content='1200' />
<meta property='og:image:height' content='630' />
<meta name='twitter:card' content='summary_large_image' />
<meta property='twitter:domain' content='gdsc-knu.com' />
<meta property='twitter:url' content={url} />
<meta name='twitter:title' content={title} />
<meta name='twitter:description' content={description} />
<meta name='twitter:image' content={image} />
</Helmet>
);
};
공용 컴포넌트를 만든 다음
import { useParams } from 'react-router-dom';
import { SEO } from '@gdsc/router/components/Seo';
export const MainMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='GDSC 경북대의 공식 홈페이지에 오신걸 환영합니다. GDSC 활동과 관련된 최신 정보와 이벤트 소식을 확인하세요.'
url='https://gdsc-knu.com'
image='https://gdsc-knu.com/WhiteLogo.png'
/>
);
};
export const SigninMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='로그인 후 서비스를 이용해보세요.'
url='https://gdsc-knu.com/signin'
image='https://gdsc-knu.com/Login.png'
/>
);
};
export const MypageMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='마이페이지 정보 확인하기'
url='https://gdsc-knu.com/mypage'
image='https://gdsc-knu.com/WhiteLogo.png'
/>
);
};
export const IntroduceMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='4기를 앞으로 이어나갈 GDSC KNU의 소개 페이지입니다.'
url='https://gdsc-knu.com/introduce'
image='https://gdsc-knu.com/Introduce.png'
/>
);
};
export const ApplyMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='GDSC KNU는 모든 경북대 학생들을 환영합니다.'
url='https://gdsc-knu.com/apply'
image='https://gdsc-knu.com/ApplyNav.png'
/>
);
};
export const ApplyFormMetaData = () => {
const { tech } = useParams();
return (
<SEO
title='GDSC KNU'
description='GDSC KNU는 모든 경북대 학생들을 환영합니다.'
url={`https://gdsc-knu.com/apply/${tech}/form`}
image='https://gdsc-knu.com/WhiteLogo.png'
/>
);
};
export const ApplyExMetaData = () => {
const { tech } = useParams();
return (
<SEO
title='GDSC KNU'
description='GDSC KNU는 모든 경북대 학생들을 환영합니다.'
url={`https://gdsc-knu.com/apply/${tech}`}
image='https://gdsc-knu.com/WhiteLogo.png'
/>
);
};
export const ApplyInquiryMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='지원하신 서류를 조회하세요.'
url={`https://gdsc-knu.com/apply/inquiry`}
image='https://gdsc-knu.com/ApplyInquiry.png'
/>
);
};
export const TeamBlogMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='빠른 시일 내에 더 좋은 서비스를 제공할 수 있도록 노력하겠습니다.'
url={`https://gdsc-knu.com/techblog`}
image='https://gdsc-knu.com/CommingSoon.png'
/>
);
};
export const TeamMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='빠른 시일 내에 더 좋은 서비스를 제공할 수 있도록 노력하겠습니다.'
url={`https://gdsc-knu.com/team`}
image='https://gdsc-knu.com/CommingSoon.png'
/>
);
};
export const CommunityMetaData = () => {
return (
<SEO
title='GDSC KNU'
description='빠른 시일 내에 더 좋은 서비스를 제공할 수 있도록 노력하겠습니다.'
url={`https://gdsc-knu.com/community`}
image='https://gdsc-knu.com/CommingSoon.png'
/>
);
};
페이지별 메타 태그를 관리하는 페이지를 만들어 태그를 관리하였다.
태생의 한계점
react-async-helmet는 주로 클라이언트 사이드 렌더링(CSR)과 서버 사이드 렌더링(SSR) 환경에서 head
태그를 동적으로 관리하는 데 유용하지만, SEO의 본질적인 문제를 해결하는 것은 아니다. 클라이언트 사이드 렌더링만 사용하는 경우, 검색 엔진 크롤러가 JavaScript를 완전히 실행할 수 없기 때문에 초기 페이지 로드 시 SEO 최적화에 한계가 있을 수 있다.
또한 서버 사이드 렌더링을 사용하는 경우, 초기 페이지 로드 시 메타 데이터가 포함되지만, 사용자 상호작용 후 페이지 상태 변화에 따라 head
태그가 업데이트되면, 검색 엔진 크롤러가 이러한 변경을 감지하지 못할 수 있다. 검색 엔진은 초기 로드 시점에서의 페이지 상태를 기반으로 인덱싱을 수행하는 경향이 있다.
그래서 여기까지 더 고민쓰,,,? React의 한계점 도달,,,?
https://github.com/Tofandel/prerenderer
그래서 url기반으로 build 타이밍 이전에 크롤링을 하여 html파일로 프리렌더링을 시켜주는 라이브러리를 사용해서 프리렌더링을 시켜주었지만 어째선지 동적 라우팅 같은 경우 /:pathname
은 제대로 빌드가 되지 않는 것 같다.
(아시는 분 해결책좀,,,)
결론
SEO를 열심히 처리하면서 느꼈던 점은 참,,,Next.js가 깡패긴 한거 같다. 이제 Next.js를 넘어갈때를 느낀것 같다,,,?라는 생각이 든다.
처음 배포때 초기 화면 렌더링 속도 및 메인 화면 성능
Desktop
Mobile
최적화 후 배포때 초기 화면 렌더링 속도
Desktop
Mobile
우선 제일 주목했던 점은 lazy로딩과 Suspense처리를 통해 최대한 초기 렌더링 속도를 줄였었다.
최대한 한 페이지내에서 겪는 컨텐츠가 아닌 것들은 다 lazy loading을 적용시켰고
이미지 같은 경우에는 처음에는 이미지를 불러오되 블러처리를 해서 불러온뒤 그다음 원본 이미지를 보여주는 이미지 lazy loading을 적용하였다.
최종적으로 8000ms -> 2000ms 4배 정도 성능 향상을 이끌어 낸 성과를 이루어냈다.
페이지가 부드럽게 돌아가는 모습을 보니 정말 기분이 좋더라,,,
메인페이지 같은 경우에는 gif -> video로 교체를 하였다. gif가 그렇게 무거운 용량을 차지하는지 이번에 처음 깨달았기 때문에 gif 교체 하나로 성능이 확실히 올라갔다는 생각이 들었다!
배포 같은 경우에는 s3 + cloudfront + route53을 활용해서 배포를 진행하였다. 실제 서비스가 운영하게 되면 cdn을 활용하는 이야기는 대충 듣고 있었기 때문에 최대한 실제 서비스처럼 배포를 하고 싶었고 pnpm을 통해서 배포를 진행하였다.
pnpm 짱짱 최고
확실히 느끼는 거지만 왜 toss가 yarn berry를 쓰는지 알 것 같다.
보통 항상 프로젝트를 진행할 때 npm으로 배포하면 대략 2~3분 정도 걸렸었다. 이 때문에 우리 서비스는 최대한 빠르게 버그나 서비스를 추가하거나 수정하기 위해서 npm 보다는 pnpm, yarn berry라는 선택지가 있었는데 조금 더 안정적으로 배포를 하고 싶다는 의견이 있었기에 pnpm을 선택하였다.
pnpm을 배포를 하면서 현재 서비스 배포 시간은 1분대로 배포가 진행이 되고 나름 빠르다는 생각을 하고 있다. 이 부분은 다른 게시글에서 자세히 다뤄볼려고 진행한다.
현재 휴식을 취하면서 요즘 구글 애널리틱스 보는 맛으로 산다.
사용자들이 늘어나고 사용자가 얼마나 이벤트를 썼을까라는 흥미로 틈만 나면 휴대폰에 애널리틱스를 킨다.
ver2는 현재 프론트엔드 팀 3명 백엔드 2명 QA 및 문서화 1명 디자이너 한명으로 가져갈 계획이다. 기능은 지원이 다 끝나고 멤버가 확정나면 구현을 시작할 생각이다.
버그 제보 같은 경우에는 gdsc.knu@gmail.com으로 제보해주면 감사하겠습니다!!
https://github.com/GDSC-KNU/GDSC_Official_FE -> 요기 스타도 좀 많이 눌러주시면 감사하겠습니다 ㅎㅎ,,
다음 편에는 GDSC-KNU 리드편으로 찾아뵙겠습니다.