STU-TI 최종 프로젝트 회고
미음제
·2022. 8. 23. 21:43
최종 프로젝트를 한마디로 요약해보자면, '최고의 팀원들과 성공적인 마무리!'라고 할 수 있다. 부족한 점도 많았던 내가 성공적으로 프로젝트를 마무리한 것은 좋은 팀원들과 함께해서였다. 물론 아쉬운 점도 있고 보완해야 할 점도 많지만 기획했던 대로 프로젝트를 마무리할 수 있었고, 팀원들에게 배운 것도 많고 새로운 도전을 성공적으로 해결한 것 같아 긍정적인 면이 더 많다. KPPL 팀원 여러분 같이 프로젝트를 진행해서 영광이었고 너무나도 감사했습니다!
프로젝트 소개
STU-TI
Study + MBTI
MBTI를 기반으로
나와 잘 맞는 스터디를 구하거나
스터디 원을 구하는 것을 도와주는 서비스 입니다.
프로젝트 링크
프로젝트 기술
프론트엔드 팀 전체가 공통적으로 사용했던 기술이다. 프로젝트 기간(7월 15일 ~ 8월 14일)을 고려해 사용할 기술을 정의했다.
내가 맡은 부분
전체적인 페이지를 작성해두고 원하는 파트에 지원을 하고 업무를 분담하기로 했다. 로그인 처리, 무한 스크롤 처리를 실제 사용해보거나 프로젝트에 도입해보지 않아 이 부분도 해보고 싶다는 생각이 들었었는데 form validation에 관해서 formik과 yup 라이브러리를 사용해 보고 싶어 스터디 생성/수정 페이지를 맡겠다고 했다. 여러 라이브러리를 사용해보는 게 좋은 경험이 될 것 같다고 생각했고, 이번 프로젝트에서 스터디 생성 form이 사이즈가 커서 좋은 기회가 될 것이라고 생각했다.
UI
전체적인 디자인을 피그마로 너무나 잘 작성해주셔서 어렵지 않게 페이지를 뽑아낼 수 있었다. 개발 기간을 고려해 디자인 적으로 시간을 줄이고자 디자인 프레임워크 Material UI를 도입해 사용했는데 이 덕분에 빠르게 끝낼 수 있었다. MUI의 기본 컴포넌트 theme color나 default value를 Mui Provider로 정의해 두어 공통적인 디자인이 있어 쉽게 개발할 수 있었다(피그마부터 Mui Provider까지 잘 작성해주신 키아님 감사했습니다).
Formik, Yup validation
스터디 생성/수정 form validation을 하기 위한 라이브러리로 formik과 yup을 사용했다. 라이브러리를 사용하지 않고 form validation을 구현하려면 input의 state값을 관리하고 onChange handler를 통해 입력 값을 검증해야 한다. input이 몇 개 되지 않는다면 큰 어려움은 없지만 input이 여러 개라면 state와 handler 함수도 여러 개를 작성해야 하는 번거로움이 있다. 이를 효율적으로 관리하고자 라이브러리를 도입하게 되었다.
Formik vs useFormik
formik과 yup 라이브러리로 form validation을 구현하기에 앞서 formik 공식 문서를 살펴보았다. formik을 사용하는 대표적인 두 방법으로 Formik 컴포넌트와 useFormik hook이 존재한다. 다른 팀원들은 useFormik hook을 사용했고 나는 Formik 컴포넌트를 사용했다. Formik을 사용했던 가장 큰 이유는 내가 보기엔 직관적이라 사용했다. 직접 작성해야 하는 입장에서 조금 더 직관적인 게 작업할 때 원활하지 않을까 해서였다.
다른 이유로는 useFormik hook에 대한 공식문서에서 hook을 사용하는 것을 추천하고 있지 않다. 문서에 따르면 useFormik hook은 context를 생성하기 때문에 Formik 컴포넌트를 사용할 수 없거나 withFormik을 사용할 수 없을 때만 useFormik hook을 사용하라고 권장하고 있다.
Formik 컴포넌트를 통해 생성한 Form Container의 JSX 구조는 다음과 같다.
<Formik
initialValues={{
isOnline: radioValues[0].value,
title: '',
topic: '',
region: '',
numberOfRecruits: '',
description: '',
}}
validationSchema={CreateSchema}
onSubmit={(values, actions) => {
// form submit handler
}}
>
{({ handleSubmit, handleChange, values, isSubmitting, touched, errors }) => {
return (
<form onSubmit={handleSubmit}>
// form 구성 요소
</form>
);
}}
</Formik>;
form의 initialValues를 정의하고, validation에 대한 Yup Schema를 주입하고, form 제출 시 어떤 처리를 할지 onSubmit에서 작성해준다. Formik 컴포넌트의 자식 컴포넌트로 form을 주입하고, handlerSubmit을 onSubmit으로 연결시켜주면 된다.
Yup
Formik의 validationSchema prop에 Yup Schema를 주입할 수 있다. Yup의 유효성 검사 오류 메시지를 key가 values, initialValues, touched filed와 일치하는 객체로 자동 변환시켜 주어 validation을 구현하는 데 있어 많은 편리함을 제공해 준다. form validation을 위한 Yup Schema는 다음과 같이 작성했다.
const CreateSchema = Yup.object({
isOnline: Yup.string(),
title: Yup.string()
.trim('앞, 뒤 공백을 제거해주세요.')
.strict()
.max(50, '50자를 넘을 수 없습니다.')
.min(5, '최소 5자 이상입니다.')
.required('제목을 입력해주세요.'),
topic: Yup.string().oneOf(topicValues).required('분야를 선택해주세요.'),
region: Yup.string().when(['isOnline'], {
is: 'offline',
then: Yup.string().oneOf(regionValues).required('지역을 선택해주세요.'),
otherwise: Yup.string(),
}),
numberOfRecruits: Yup.string()
.oneOf(recruitsNumberValues)
.required('인원을 선택해주세요.'),
description: Yup.string()
.trim()
.max(1000, '1,000자를 넘을 수 없습니다.')
.min(10, '최소 10자 이상입니다.')
.required('스터디 내용을 입력해주세요.'),
});
validation schema에 Yup Schema를 적용하면, form을 제출할 때 이 Schema를 토대로 validation을 진행하게 된다. validation이 끝나야 Formik 컴포넌트의 onSubmit 함수가 실행된다. 그 전에는 onSubmit 함수로 넘어가지 않게 된다.
- string() : 입력값이 문자열인지 검증
- min(), max() : 입력값의 최소, 최대 길이 검증
- trim() : 앞, 뒤 공백을 제한다. 공백을 제한 후 mix(), max() 검증
- strict() : 역시 공백을 제하고 min(), max()를 검증하는데, 공백이 2번 연속되지 못하도록 제한
- required() : 반드시 필요한 field에 대한 검증
- oneOf() : 매개 변수의 값 들 중 하나에 속하는지 검증, 배열 형태
- when() - is, then, otherwise : 조건 검증, when에 속하는 field 값이 is에 적힌 값인 경우 then 내용을 검증, 아닌 경우 otherwise 내용을 검증
- 각 메서드 내부에 적혀있는 메시지가 validation에 실패했을 때 노출될 error 메세지가 된다.
Formik과 Yup 라이브러리 적용 결과
Formik 컴포넌트가 정상 작동하는 것을 확인하고, Yup Schema validation에서 when을 사용하여 validation을 강화시키고, MUI TextField의 helperText를 공통으로 사용하기로 결정한 뒤 최종 리팩터링을 거쳐 마지막 모습이 완성되었다.
새롭게 배운 내용
Typescript
그동안 TS를 사용하지 않고 과제나 프로젝트를 진행해오다 이번 최종 프로젝트에서 TS를 도입하기로 결정했다. 그동안 타입을 작성하지 않고 작업을 했던 터라 매번 타입을 작성하는 일이 매우 어렵고 번거로웠다. 이렇게 작성하면 되겠지..? 하고 작성하면 어김없이 TS는 나에게 훈수를 해주었다.
TS를 배운 지 얼마 안 된 시점에서 프로젝트에 도입하는 게 잘 선택한 일일까? 의문이 많이 들었지만 지금 생각해보면 잘 도입했다고 생각한다. 위 사진과 같은 예시에서 예상치 못한 에러를 JS만 사용했더라면 런타임에서 에러를 발견하겠지만, TS를 도입해 코드 작성 단계에서 미리 확인할 수 있었다. 이런 친절한 훈수 덕분에 FormData에서는 string과 blob 타입만 허용되는 것도 알게 되었다(string이 아닌 경우 자동으로 string으로 파싱 한다고 한다. TS는 그전에 네가 알아서 해결해서 넘겨라라고 훈수!).
그리고 여러 컴포넌트를 작성하면서 prop을 내려줄 때 prop 타입을 지정하다 보니 prop 전달 과정에서 미리 에러를 잡고 갈 수 있어 좋았고, 어떤 형태의 prop이 옮겨지는지 예측할 수 있어 좋았다. 특히 함수의 경우 사람마다 지정하는 네이밍 방식이 상이한데(누구는 handler, 누구는 onSubmit 등) 이 또한 TS를 통해 타입을 명시해주다 보니 명확하게 파악이 되었던 것 같다.
readAsDataURL, createObjectURL
내가 맡은 스터디 생성/수정 페이지와 팀장님이 맡으신 커뮤니티 포스트 작성에서 이미지 업로드를 다룬다. 여기서 나는 readAsDataURL을 사용했고, 팀장님은 createObjectURL을 사용했다. 두 방법 모두 브라우저에서 로컬 파일을 읽어 사용할 때 활용하는 방법으로 썸네일 이미지를 보여주기 위해 사용했다.
우선 내가 작성한 방법은 다음과 같다. input을 통해 파일을 받고, 해당 file을 보여주기 위해 readAsDataURL을 사용한다.
const encodeFile = (fileBlob: File) => { // 썸네일 이미지를 보여주기 위한 함수
const reader = new FileReader();
if (!fileBlob) return;
setIsLoading(true);
reader.readAsDataURL(fileBlob);
return new Promise<void>((resolve) => {
reader.onload = () => {
const result = reader.result as string;
setThumbnailImage(result);
resolve();
setIsLoading(false);
};
});
};
파일을 비동기로 읽기 위해 FileReader 객체를 생성자 함수로 생성하고, readAsDataURL를 통해 해당 파일을 Base64로 인코딩 시킨 문자열을 생성한다. 그리고 onload를 비동기로 실행시켜 인코딩 된 문자열을 state로 저장하고 JSX 부분에서 해당 state 값을 노출시키면 썸네일 이미지가 노출된다.
createObjectURL을 사용하면, input을 통해 파일을 받고 URL.createObjectURL() 메서드를 통해 해당 파일 객체를 가리키는 URL을 DOMString으로 반환한다. 이때 반환된 값은 브라우저를 닫기 전까지 유지된다.
썸네일 이미지를 보여주기 위해 사용하는 것은 동일하지만 둘의 차이는 분명하다. 가비지 콜렉터에 의해 수거되는 readAsDataURL, 직접 revokeObjectURL을 실행해 수거해야 하는 createObjectURL. 더 빠른 속도를 보이는 createObjectURL, 그리고 최대 사용 용량 차이 등 차이가 극명하다. createObjectURL이 더 효율적인 방안인데 미처 생각하지 못하고 readAsDataURL를 사용했다. 팀장님이 readAsDataURL를 사용하지 않았더라면 찾아보지 못하고 넘어갔을 것 같다. 이 부분에 대해 더 자세한 내용은 나중에 따로 정리해서 포스팅하도록 해야겠다.
Git Rebase
지난 중간 프로젝트에서는 develop 브랜치가 최신화가 되면 바로 pull을 받아와 작업하고 다시 merge 하는 방식으로 작업했다. 바쁘게 작업을 하는 탓에 구두로 PR 승인을 받고 바로 merge하는 방식으로 작업했고, develop이 최신화될 때마다 해당 내용을 pull 하여 작업하다 보니 굉장히 복잡했다.
이렇게 복잡한 그래프보다 깔끔한 그래프가 변경 사항을 추적하는데 용이하기 때문에 최종 프로젝트에서는 rebase를 사용하기로 결정했다.
rebase를 통해 최신화된 develop 내용은 가져오되 머지 버블은 없애고 최신화 된 내용 뒤로 현재 내가 작업하고 있는 브랜치 커밋을 옮겨주게 된다.
rebase 개념이 생소했던 터라 프로젝트 초기에 애를 먹기도 했다(vsc github 연동 이슈도 있었다. 로그인 연동이 계속 해제되어 브랜치 이동이 안된 상태로 rebase, pull 명령어가 입력되어 매우 매우 꼬였었다).
file changed도 모두 추적되어 PR이 불가능한 상태까지 이르게 되었고, 새로운 브랜치를 파서 다시 PR을 작성했다. 너무나 수고스러운 일을 했다.. rebase 개념을 정확하게 인지하고 CLI 명령어를 숙지한 상태로 작업했으면 안 그랬을 텐데 하는 아쉬움이 있었다. 그래도 한번 호되게 당하고 난 후 프로젝트를 마칠 때까지 문제는 없었다.
디자인 패턴
이것도 역시 중간 프로젝트에서 챙기지 못했던 부분이다. 중간 프로젝트 피드백 당시 atomic 패턴을 고려해보면 좋다는 얘기를 들었다. 이 얘기도 최종 프로젝트 기획 당시 나왔었는데, 짧은 기간 내에 추상화가 어려울 것이라 판단했고 패드님이 제안해주신 Components-Container-Page 구조를 도입하기로 결정했다.
전달되는 prop에 때라 UI 변경만 있는 경우 Component로 작성하고, 여러 Components를 조합해 새로운 Container를 만든다. Container에서는 실질적인 Data를 핸들링하게 된다. 다시 여러 Containers를 조합해 새로운 Page를 만들고, 이때 Page가 사용자에게 노출되는 페이지가 된다.
처음에는 Components와 Containers를 분리하는 게 굉장히 머리 아프고 복잡하다 생각했는데, 데이터를 핸들링하는 여부에 따라 구분 지어 생각하니 복잡한 머리가 정리된 것 같고 상향식 개발을 처음 접하게 된 것 같다. 중간 프로젝트에서는 한 페이지를 모두 작성하고 내가 분리할 수 있을 만큼 분리하는 하향식 개발 구조였던 터라 재사용성이 높은 Components를 만드는 게 어려웠었는데 이번 최종 프로젝트로 많은 연습이 되었던 것 같다.
백엔드와 소통
데브코스를 진행하면서 가장 기대했던 최종 프로젝트. 그 이유는 '백엔드 분들과 같이 진행하는 프로젝트'라서 였다. 그간 프로젝트 경험도 없었고, 더더욱 백엔드 분들과 소통할 일이 없었는데 이번 프로젝트를 통해 많은 경험을 했다.
기획 당시 어느 정도 프로젝트 구상이 되었다 싶었을 때쯤 백엔드 분들께서 "생각보다 저희 할 일이 많지 않아요. 오히려 중간 프로젝트보다 도메인이 적어서 업무를 나누는 게 어려울 정도예요"라고 말씀해 주셨다. 프론트끼리 일정을 고려해 잘 설계한 것 같았는데, 백엔드 분들도 최종 프로젝트인(프론트와 함께하는) 만큼 새로운 도전 또는 더 많은 시도가 필요했을 텐데 그 점을 간과했던 것이다. 초기에 너무 백엔드 분들을 생각하지 않고 기획을 한 것 같아 아찔했다. 그래도 백엔드 분들이 솔직하게 의견 제시도 해주시고 프론트 의견도 적극 수용해 주셔서 원만한 타협점을 찾아 개발을 시작할 수 있었다(그렇죠 백엔드 여러분..?).
개발을 하면서도 많은 소통의 시간을 가졌다. 중간 프로젝트 당시에는 API 명세가 작성된 상태로 프로젝트를 진행했던 터라 어떻게 API를 활용할지, 주어진 상황에서 어떻게 발전시킬지 만을 고민했었다. 최종 프로젝트에서는 백엔드 분들의 작업에 따라 다시 협의해야 할 사항도 생겨났다. 이때 내가 인지하고 있는 점, 전달하고자 하는 점을 정확하게 전달하는 게 어렵다고 느꼈다. 혹시 잘 못 이해하고 있거나, 잘 못 전달하게 되면 다시 수고스러운 일을 해야 하니 많이 생각하고 고민했던 시간이었다.
백엔드 지식이 부족한 터라 소통에 문제가 있진 않을지, 서로 이해관계가 달라 대립하는 경우가 많진 않을지 걱정이 되었지만 생각보다 좋은 호흡으로 프로젝트를 마무리했던 것 같다. 팀원들이 백엔드에서 전달받은 내용을 이해하기 쉽게 다시 공유해주시고, 백엔드 분들도 프론트 의견도 적극 수용해주시고 솔직한 의견도 많이 제공해 주셔서 무리 없이 소통하며 마무리할 수 있었던 것 같다. 다만, 백엔드 쪽에 대해 아는 게 부족해 자주 물어봤던 것이 죄송하다. 간단한 것인데도 잘 몰라서 많이 물어봤던 것 같다. 질문하지 않는 것보단 빨리 물어보고 해결하는 게 좋지만, 어느 정도 백엔드 지식이 필요하다고 느끼게 되었다.
최종 프로젝트 회고
너무나 좋은 팀원들을 만나 행운이었고, 잘하시는 분들과 같이 프로젝트를 진행하며 많이 배우게 되었다. 이벤트성 이긴 하지만 평가 1등도 하고(게임 1등 했을 때부터 예정된 일..?) 값진 경험을 했다. 특히 프로젝트 과정에서 직면한 문제를 어떻게 해결하고자 하는지, 무엇을 고민하는지를 많이 보고 배웠다. 스스로 아쉬운 점을 발견하고 어떻게 개선하고 싶은지 고민할 수 있게 성장한 것 같아 정말 정말 값진 경험이라고 생각한다.
두 번째 단점은 완벽하게 진행하고 싶은 탓에 용기를 내지 못하는 경우가 있다는 것입니다. 어떤 일을 시작하고 진행하기 위한 능력이 1부터 100중 70만 요구된다고 하더라도, 70의 능력이라면 마무리하기에 조금 부족하지 않을까 하는 걱정부터 하게 됩니다. 일을 완벽함에 가깝게 마무리하는 것도 좋지만, 지나친 걱정 때문에 할 수 있던 경우에도 용기를 잃고 시도조차 하지 않은 때도 있습니다. 이번 과정도 따라갈 수 있는 실력이 있는지, 다른 사람들과 같이 학습할 때 방해가 되지 않을지 사소한 걱정들이 앞서는 느낌입니다. 부족한 만큼 더 노력하고 시간을 투자해 부족한 부분을 채우고, 도전하면서 용기를 먼저 낼 수 있는 사람이 되도록 노력하겠습니다.
데브코스 지원 당시 내 단점으로 적었던 내용이다. "새로운 것을 도전하는 것에 대한 두려움이 있다. 이를 데브코스에서 해결하고 싶다."라고 작성했었는데, 도전하는 게 두려운 것이 아니라 재밌는 것이라고 느끼게 된 것 같다. 부딪히며 깨져도 보고, 새로 배우고, 어려운 부분은 도움도 받고, 더 나아가 내가 다른 사람을 도와줄 수 있게 발전하는 과정을 배운 것 같다.
아직도 많이 부족하고 배워야 할 것들이 산더미다. 너무나도 잘하시는 분들이 많았고 그분들이 얼마나 많은 노력을 했는지 볼 수 있었다. 최종 프로젝트를 포함한 약 5달 간의 데브코스 과정을 통해 많이 깨우치게 되었고 빨리 성장해서 같이 고생했던 동료들과 회사에서 다시 만나기를 기원한다!!
참고
'프로그래머스 > 데브코스 프론트엔드' 카테고리의 다른 글
[TIL] 2022-07-06 / 78일차 (useReducer) (0) | 2022.07.06 |
---|---|
[TIL] 2022-07-01 / 75일차 (Context API) (0) | 2022.07.01 |
[TIL] 2022-06-28 / 72일차 (SPA역사와 SSR) (0) | 2022.06.28 |
프로그래머스 데브코스 중간 팀 프로젝트 회고 (0) | 2022.06.27 |
[TIL] 2022-06-23 / Day 69 (타입스크립트 기본 문법) (0) | 2022.06.23 |