메인프로젝트는 팀장 역할과 프론트엔드 개발을 함께 진행하며 코드 리뷰와 진척도를 체크하고 프로젝트 중간 중간에 제출을 담당하게 되었다.
팀명: sixman
프로젝트의 목표가 여러 서비스를 혼합한 헬스 케어 웹서비스로 결정된 후 첫 아이디어 회의에서는 굉장히 많은 아이디어가 오갔다.
ex) 운동인들을 위한 커뮤니티 서비스, 매일 식단을 추천하고 연계하여 당일 섭취한 칼로리를 기반으로 일일 적정 칼로리를 계산해주는 서비스, 운동인들만의 SNS, 건강기능 식품을 광고하고 판매를 연계하는 서비스 등
많은 아이디어 속에서 기술 스택과 시간적 제약, UX의 통일성을 고려하여 유저 데이터를 기반으로 신체정보 통계 및 건강관리 기능을 제공, 커뮤니티를 통해 유저 간 정보 공유 및 교류, 주변의 운동시설을 안내해주는 위치 기반 서비스를 제공하는 헬스 종합 플랫폼 사이트로 정리하게 되었다.
사이트의 플로우와 파일 구조를 작성해서 각자 담당할 파트를 나누고 폴더명, 파일명, git 메세지 통일 등 다양한 룰을 설정했다. 이 중에서 나는 메인(통계보드) 영역과 캘린더 영역을 담당하기로 했다. 시각화 차트와 캘린더 api 등 도전해보고 싶었던 기술이 많았었기 때문에 욕심을 내 먼저 하겠다고 목소리를 내보았는데 다행히도 다른 팀원들의 양해를 받아 불협화음 없이 진행할 수 있게 되었다.
새로운 도전, TypeScript와 Next.js
메인프로젝트에서는 기존에 시도해보지 않았던 기술 스택들을 정리해나갔다. 선행학습되지 않은 TypeScript와 Next.js를 도입하기로 했는데 TS의 경우 많은 기업에서 사용하고 있기도 하지만 런타임 에러를 실시간으로 확인하며 그때그때 발생하는 오류를 해결해나가는 경험을 여러번 해본 결과 오류 해결의 근본적인 원인을 정확히 알지 못하는 경우가 많았는데 코드를 작성하는 단계에서 컴파일 에러를 통해 잘못된 코드임을 인지하고 수정해나가는 것이 튼튼한 집을 만드는 것처럼 완성도 있는 프로젝트를 구축해 나가는 과정에 필수적이라고 생각해서 도입했다.
NEXT JS는 SSR이 주는 이론적인 이점만을 생각하여 도입했지만 SEO가 아직(?) 불필요한 점, 프론트에서 따로 API를 만들지 않는 점 등으로 필요성을 확 실감하진 못했지만 새로운 기술 스택을 도입하고 NEXT의 유용한 기능들을 사용함에 만족했다. 스타일시트 파일의 경우 코드 유지보수와 폴더 정리를 우선으로 고민하다가 styled component와 css.module 사이에서 css.module을 선택하게 되었다.
내가 맡게 된 파트는 다음과 같다.
Main Page(User Info Section): D3 Library를 사용하여 User Data 중 Kcalory & Weight를 시각화
Main Page(Calendar Info Section): User 고유의 Calendar API에 저장되어 있는 DB에서 Todolist에 저장된 TItle, Contents, Kcal 데이터를 조회
Main Page(Diet Info Section): User의 신체 데이터를 기준으로 Chat GPT API와 연동된 BackEnd Chat API에 오늘의 식단 요청 및 응답을 시각화
Main Page(Community Info Section): Community Page의 오운완 카테고리에서 조회수+좋아요 점수가 가장 높은 Best3 게시글을 응답받아 각각의 상세페이지로 이동
HelChat: Main Page(Diet Info Section)에서 사용했던 Chat GPT API와 연동된 BackEnd Chat API를 다시 활용하여 ChatBot 형태로 구현
Calendar Page: DayPicker Library를 사용하여 구현한 Full Calendar에 Todolist 형태의 컴포넌트를 결합하여 Calendar Todolist 구현
진행 방향
생각보다 많은 선택지가 있었던 캘린더 라이브러리 중 팀원의 추천으로 fullcalendar을 먼저 사용하게 되었는데 도저히 css 수정하는 방법이 감이 오지 않아 커스텀이 자유롭고 많은 오픈소스가 존재하는 react-datepicker 라이브러리를 선택했다.
react-datepicker는 완벽히 달력기능에만 초점이 맞춰져 있고 그 외 커스텀은 달력 외의 인풋창이나 사이드 창에서 이루어졌기 때문에 처음 기획대로 당일 칼로리를 달력 내부에 표시하는 방법을 찾을 수가 없었다.
우선 필수적인 기능 중 해당 날짜에 메모 기능 + 칼로리 저장 기능에 대한 CRUD 구현을 우선으로 작업을 진행했다. calendar Api는 제목, 내용, 칼로리, 생성일자를 body에 담아 요청하고 path 뒤에 ID를 넣어 제목, 내용, 칼로리, 생성일자를 응답받는다.
[{
calendarId: 21
content: "닭가슴살 샐러드, 고구마 2개, 삶은 계란 2개"
kcal: 1440
recodedAt: "2023-03-08"
title: "오늘의 기록"
}]
api가 정상적으로 작동하는 것을 확인한 후 popup component에 조회,추가,수정,삭제 기능을 구현하였다.
아쉬운 점
next.js는 css module을 사용하여 각 컴포넌트와 라우팅 페이지를 스타일링한다. 외부 라이브러리를 사용하는 경우 module css가 아닌 최상위 app.tsx에 import된 css에서 커스텀하고자 하는 클래스명을 호출하여 important로 우선순위의 속성값을 부여하는 방식으로 작업이 가능한데 fullcalendar로 작업했을 당시 이런 적용이 능숙하지 않아 커스텀을 포기하게 되었다. 초기 기획했던 메모 기능과 캘린더 내부에 기록된 데이터를 노출시키는 기능을 제공해주는 라이브러리는 fullcalendar가 더 적합하다고 생각해서 코드 리펙토링을 통해 라이브러리를 교체할 예정이다.
또한 직접 Kcal 데이터를 입력해야 하는 점은 사용성이 떨어진다고 판단했다. 오늘 섭취한 음식이나 식단을 고르면 자동으로 Kcal 데이터가 들어올 수 있도록 식단 리스트 페이지를 따로 작성하는 것이 기능적으로 더 완성도 있을 것 같아 향후 수정할 내용에 추가하려고 한다.
진행 방향
유저데이터 시각화, 캘린더 조회, 식단 추천, 커뮤니티 솔팅 네가지의 영역을 보드형태의 페이지로 구성했다. 먼저 로그인 과정에서 로컬스토리지에 담긴 accessToken을 받아와 mypage에서 저장된 유저 데이터를 조회했다. 토큰값이 없으면 로그인으로 이어주는 컴포넌트를 노출시키고 토큰이 존재할 경우 user nickname을 담은 인사말을 상단에 띄우고 비로그인 페이지와 구분하고 모든 서비스를 노출시킨다.
유저데이터 시각화 - user의 physical Api의 weight 정보, calendar Api의 kcal 정보를 따로 받아 d3의 Bar chart를 사용하여 구현했다. chart component를 하나로 통일하려고 했지만 kcal 차트에서 목표 칼로리를 line으로 넣어줘야 하는 경우가 있어서 두개의 파일로 분리시켰다. 마우스 오버 시 각 그래프에 대한 데이터가 tooltip으로 노출되는 기능은 레퍼런스를 찾아 제작하게 되었다. user의 physical Api가 응답되지 않으면 차트에 아무 정보도 담기지 않는데 이 경우, chart component 대신 마이페이지로 이동시켜주는 component를 대신 랜더링한다.
캘린더 조회 - calendar 페이지의 기능 중 Read 기능만 메인페이지로 가져왔다. React-datepicker는 캘린더를 띄울 수 있는 form 기능이 내장되어 있는데 직접 구현해보고 싶어서 date form 태그를 사용하여 직접 만들게 되었다. date form에서 날짜를 선택하게 되면 '0000-00-00' 형태의 string 값이 저장되는데 calendar api의 recodedAt의 값과 일치하면 title, content, kcal 데이터를 조회하는 식으로 제작했다.
식단 추천 - 비회원이면 로그인 유도, 회원일 경우 평균 식단 추천, physical Api에 키, 몸무게, 활동 지수 등의 신체 정보가 기입되어 있는 경우 맞춤 식단 추천이 뜰 수 있도록 분류했다. Chat GPT open API와 연결된 백앤드 서버의 ai api로 두가지 경우에 대한 질문을 보내면 각각 다른 응답을 전달받는다는 테스트 결과를 통해 코드를 작성했고 2020년까지의 정보가 학습되어 있는 Chat GPT는 정량적인 데이터에 대한 질문은 비교적 정확한 응답을 보내주었다.
const question =
userData !== null && userData.gender !== undefined ? `나는 ${userData.gender}이고 하루 운동량은 ${userData.activityLevel}이야. 하루 권장 소비 칼로리 ${userData.result}kcal일 때 ${userData.goal} 식단 나열하고 총 칼로리까지만 알려줘.` : '일반적인 다이어트 식단 나열하고 총 칼로리까지만 알려줘.';
그러나 요청과 응답 사이에 텀이 길어지는 문제가 발생했다. 확인해보니 Chat GPT open API로 백앤드 서버가 요청을 보내면 미리 설정한 양만큼의 답변을 작성하고 이를 번역해서 한번에 응답으로 내보내는데 이 때 돌아오는 시간이 딜레이되었던 것. 응답받는 답변을 state로 담아 state가 비어있는 문자열일 경우 로딩 컴포넌트를 반환하도록 조정해주었다.
커뮤니티 솔팅 - community API는 좋아요 갯수와 조회수를 합산하여 각 카테고리 별로 게시글 순위를 측정한다. user login 시 access token이 확인되면 오운완 카테고리의 게시글 중 1위부터 3위까지의 글을 응답받아 적용했는데 텍스트에디터를 통해 작성된 글이 서버로 송신될 때 특수문자나 띄어쓰기 같은 경우 마크업 언어 그대로 노출되는 이슈가 발생했다.
ConvertToHtml.tsx라는 기능 컴포넌트를 따로 만들어 마크업 언어가 그대로 노출되는 페이지에 재활용하기로 결정했다.
const escapeMap = {
'<': '<',
'〈': '<',
'>': '>',
'〉': '>',
'&': '&',
'"': '"',
''': "'"
};
const pattern = /&(lt|gt|amp|quot|#x27|#12296|#12297);/g;
const reg = /<[^>]*>?/g;
export const ConvertToHtml = (text) =>
text
.replace(pattern, (match, entity) => escapeMap[`&${entity};`] || match)
.replace(reg, '');
다시 살펴보니 api를 요청할 때 access token을 헤더에 담을 필요없는 API기 때문에 불필요한 과정이었다고 생각된다. 프로젝트 리펙토링 시 token을 props로 받고 요청하는 로직은 삭제할 예정이다.
아쉬운 점
비회원일 경우 처음 마주하는 메인페이지는 위의 화면이다. 대시보드이기 때문에 각 영역을 살려 로그인을 유도하는 컴포넌트를 반복적으로 랜더링하였는데 프로젝트 피드백 시 UX에 안좋은 영향을 준다는 의견이 대다수여서 사이트 리펙토링 시 처음 방문한 유저가 어떤 서비스인지 알 수 있도록 소개 페이지 형식의 페이지를 노출시킬 계획이다. 전체적으로 read에 기반이 된 페이지인 만큼 tooltip이나 명확한 서비스 소개를 통해 각 섹션이 어떤 역할을 하고 있는지 명확하게 전달하는 기능이 필요하다고 느껴진다.
진행 방향
main page의 식단 추천 기능에 사용했던 ai api가 더 활용될 수 있는 방향을 고민하던 중 트랜디한 기술을 적용한 프로젝트인 만큼 chat GPT와 자유롭게 대화할 수 있는 챗봇을 만들면 어떨까? 라는 추가적인 아이디어를 시작으로 HelChat을 만들게 되었다. 다른 사이트의 챗봇 기능을 보니 사이트에 대한 Q&A를 선택하면 답변을 제공하거나 실제 상담원과 소통하는 창구로 사용되었다. 기존 서비스와 차별화하여 Chat GPT를 기반으로 한 Chatbot과 자유롭게 질의응답을 주고받을 수 있는 서비스로 기획하게 되었다.
figma로 그린 chatbot 아이콘
채팅창의 첫 문장은 Helchat의 인사말을 Defult로 넣었다. 이후 user와 chat GPT간의 상호작용을 type: question / answer로 분류하고 object형 배열에 담아 stste로 저장했다.
const [speech, setSpeech] = useState([
{
type: 'answer',
content: '안녕하세요. 고객님. 제 이름은 헬쳇이에요. 뭐든지 물어봐주세요.'
}
]);
//.. 중략 ..
const onSubmit = () => {
axios
.post(`${url}/api/v1/ai/question`, {
question: input
})
.then()
.then((res) => {
setAnswer(res.data.body.data.choices[0].message.content);
})
.catch((error) => alert('내용을 입력해 주세요.'));
setSpeech([
...speech,
{
type: 'question',
content: input
}
]);
setInput('');
};
//.. 중략 ..
useEffect(() => {
answer === null
? null
: setSpeech([
...speech,
{
type: 'answer',
content: answer
}
]);
}, [answer]);
대화가 늘어나서 영역을 벗어나면 채팅창의 특성 상 하단 고정으로 스크롤이 늘어나야 하는데 이 부분은 대화창이 추가될 때 useEffect가 실행되도록 처리하여 scroll event를 주었다.
useEffect(() => {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [speech]);
아쉬운 점
질문을 등록하면 엔터가 한번 더 입력된다. 제출 기능을 수행하는 엔터와 띄어쓰기를 수행하는 엔터가 동시에 적용되는 것으로 shift키와 엔터가 눌렸을 때만 띄어쓰기가 적용되도록 코드를 수정해야 한다. 그리고 응답에 걸리는 시간동안 아무런 반응이 없어서 식단 추천 기능처럼 팬딩 상태에 로딩 중이라는 인터렉션을 주어 UX를 개선해야 할 것 같다.
AWS 기반으로 https.helfit.life로 배포가 완료됐다. 기술 발표를 통해 담당했던 부분을 한번 더 이해하며 부족했던 부분과 얼마나 한달동안 프로젝트에 전념했는지 돌아보게 되었다. 흔히들 백앤드와 프론트엔드 간의 소통에서 불화가 잦다고 얘기했지만 예상과 다르게 불협화음 한번 없이 좋은 인연과 좋은 프로젝트, 두마리의 토끼 모두를 잡게 됐다. 모를 때 모른다고 도움을 요청할 수 있는 용기가 프로젝트를 마무리할 수 있는 원동력이었다고 생각한다.
이번 프로젝트를 통해 처음 사용하는 기술 스택에 대한 완전한 이해가 정말 중요하고 꾸준히 복기하지 않으면 어제 작성한 코드도 낮설게 느껴진다는 점을 많이 배웠다. 취업이라는 목표를 위해 만든 과정같은 프로젝트지만 처음 완성된 내 결과물이기 때문에 꾸준한 리펙토링을 통해 더 완벽한 서비스를 제공할 수 있도록 노력할 것이다.
Github Link
깃허브 링크
Website Link
배포링크
youtube Link
기술발표 영상