'세 줄 독후감' 서비스를 시작했다. (https://www.sejulbook.com)
1명의 개발자(본인), 1명의 데이터 분석가가 함께 만들어가는 프로젝트로, 실제 서비스를 운영해보며 지속적으로 사용자 유입을 늘리는 것이 목표다.
그리고 실제로 4월 2일 서비스 시작 후 5월 3일 현재, 27명의 누적 가입자를 확보했다 🥳
실제 서비스를 운영하며 발생하는 문제와 피드백을 통해 기능 및 성능을 개선하는 등의 코드 리팩토링 활동은 스스로의 성장에 큰 도움이 되고있다.
따라서 지속적으로 서비스 관련 문제를 해결하거나, 더 나은 사용자 경험을 위해 기능과 성능을 개선하고, 개선 여지가 있는 코드를 변경/수정할 것이다.
하길 잘했다!
왜 시작했나?
'네이버 커넥트재단 부스트캠프 웹·모바일 7기'에서 그룹 프로젝트를 수행하며, 프론트엔드 개발 관련 아쉬운 점과 개선하고 싶은 것이 많았다.
지난 프로젝트에서의 실패를 통해 배운 것을 새로운 개인 프로젝트에서 적용하며 또 다른 실패를 하고 싶었다.
'2022년 하반기 회고 - 꾸준했던 시간' 글에서의 아래 회고를 실제로 이행한 것이다.
또한 부스트캠프를 통해 서버 개발 관련 역량을 강화했기에, 처음으로 혼자서 풀스택 개발을 해보고 싶었다.
(지금 보니 서버 코드는 개선해야 할 부분이 많은 것 같다..🤣)
이처럼 개인 프로젝트를 시작하기로 마음을 먹었으나..! 문제는 아이디어였다.
무엇을 만들어 볼까..
그러던 어느날 한 친구의 책 블로그를 구경하다가 '세 줄 독후감'이라는 컨셉으로 독후감을 작성하는 것을 보았는데, 이 것이 나에게는 굉장히 인상 깊었다.
책을 기록한다는 것에 대한 부담감이 늘 있었고, 누군가의 긴 독후감을 읽는 것조차 귀찮았다.
그럼에도 책은 늘 나에게 자극과 원동력을 주기에 기록의 필요성을 느끼고 있었다.
'세 줄 독후감'은 책을 기록하고 읽는 것에 대한 부담감을 낮추어 줄 수 있을 것 같았기에(마치 '세 줄 요약'처럼) 해당 프로젝트의 아이디어로 선정하게 되었다.
그리고 나에게 영감을 준 블로그를 운영하는 친구가 데이터 분석가로 합류했다.
'나 홀로 개발'의 두려움
나 홀로 개발은 처음이다. (개인 학습 제외)
늘 팀원들과 그라운드 룰, 컨벤션, 브랜치 전략 등을 협의하고 프로젝트를 설계했었다.
'팀' 이라는 울타리에서 벗어나 혼자서 개발을 하려고 하니 막막했다..
"그냥 내 마음대로 하자! 어떻게든 되겠지!" 하고 시작하려다, 나만의 룰과 전략을 수립하지 않으면 스스로의 성장에도 도움이 되지 않을 것 같았다.
결국 팀 프로젝트에서 팀원들과 했던 것을 혼자서 해보기로 했다.
코드 네이밍 규칙, 커밋/브랜치/Issue/PR 컨벤션, 브랜치 전략, 디렉토리 구조 설계 등 팀원들과 함께 고민하고 수립했던 것들을 혼자서라도 했다.
나만의 룰을 정하고 지키는 연습이 나중의 팀 협업에서 조금이라도 도움이 될 수 있을 것 같았다.
그 다음 문제는 '개발'이었다.
모든 것을 혼자서 개발하려니 막막했다.
데이터 분석가와 약속한 마감 기한을 생각하니 너무나 조급해졌다..
전략이 필요했다.
- 서버리스 웹 애플리케이션
서버를 설정하고 관리하는데 신경을 끄기로 했다.
즉, 서버리스(Serverless)를 사용하기로 한 것!
💡 서버리스(Serverless)?
서버가 없는 것이 아니라 직접 서버를 관리하지 않음을 의미한다. Backend를 서버에 올리는 것이 아니라, Backend를 작은 함수단으로 쪼개서 직접 관리하지 않는 서버(ex. AWS Lambda)에 올린다. 모든 함수는 잠들어 있고, 요청이 오는 순간 특정 함수를 깨워 작업을 수행한다.
AWS Lambda를 기반으로 하는 FaaS(Functions as a Service) 플랫폼인 Vercel을 사용하여 애플리케이션을 배포했다.
덕분에 코드에만 집중할 수 있어 쉽고 빠르게 프로덕션을 개발할 수 있었다.
다만, 서버를 설정하고 관리해본 경험이 없었기에 조금 아쉽기도 하다.
데이터 분석가와의 협업
개발 직군이 아닌 분야와의 협업으로 여태껏 수행한 프로젝트와는 완전히 다른 새로운 경험을 얻었다.
(여기서 데이터 분석가 팀원에게 '존칭'을 쓰지 않는데, 친한 친구이기 때문이니 무례해 보여도 이해해 주시기 바랍니다)
- 요구사항
다른 분야의 직군과 함께 프로젝트 기획을 진행하니 생소하고 재밌는(?) 요구사항을 받았다.
기억에 남는 것 중 하나는 '비즈니스 로직에 사용되지 않는 사용자 정보(이메일, 성별, 연령대)를 DB에 저장하기'이다.
후에 사용자 정보 데이터를 분석하고 인사이트를 도출하여 사용자 유입을 늘리는 등 프로덕션을 개선하는데 활용될 수 있기 때문이었다.
평소 일체 고려하지 않은 부분이었는데 다른 발상으로 접근하여 개발에 영향을 주니 신기했다.
실무에서는 다양한 직군과 함께 협업하니 이러한 요구사항을 많이 접하겠지..?
- DB 설계
데이터 분석가 팀원이 DB 설계를 해보고 싶다고 하여 흔쾌히 승낙했다.
가독성
설계된 ERD를 확인하니 명확하지 않은 컬럼 이름으로 인해 이해되지 않거나 소통에 문제가 생기는 경우가 발생했다.
크게 두가지가 있었는데, 첫번째는 독후감의 임시저장/발행 여부에 대한 컬럼 이름이 divide로 명시되어 있던 것이다.
무슨 의미인지 쉽게 파악하지 못해 ERD 해석에 여러움을 겪었다.
그리고 두번째는 서재(= 사용자) 구독을 위해 follow 테이블 내 following, follower 컬럼이 있었는데, 이는 우리의 소통에 문제를 야기했다.
두 컬럼 이름이 비슷해서 소통에 자꾸만 방해가 되었다. 너무 헷갈렸다..
이로 인해 서로의 의견에 오해가 생기기도 했지만, 결국 '컬럼 이름의 비슷함' 이라는 문제를 서로 인지하여 명확한 기준을 명시함으로써 문제를 완화할 수 있었다.
이 경험 때문에 나 홀로 개발임에도 누군가와 함께 한다고 생각하고 가독성 있는 코드 작성을 연습하기 위해 노력했다.
성능
DB 설계에 대해서 정~말 많은 회의를 했다. 그 이유는 '성능' 때문이었다.
나로서는 효율적인 쿼리를 통해 데이터를 불러오는 성능을 중요시 여기고 있었는데, 데이터 분석가 팀원은 이를 고려하지 못하는 것이 당연하다..
하지만 성능은 포기할 수가 없었다!
그래서 피드백을 끊임없이 했고, 그 결과 만족할만한 ERD가 나왔다.
개발 측면의 견해를 이해하고 피드백을 곧바로 변영하며 열심히 임해준 데이터 분석가에게 고맙다 🙏
실패로 배우기
"빠르게 실패하기"
이번 프로젝트에서도 당연히 아쉬운 점들이 있다. 이것을 난 굳이 '실패'라 부르겠다.
실패라는 단어가 나에게는 기분 좋게 다가온다. 내가 더 배우고 성장할 수 있다는 기대감 때문이다.
그래서 난 이번에 어떤 실패들을 했을까?
스스로의 코드를 최대한 객관적으로 살펴보며 피드백하여 선정한 것들 중 두가지만 소개해보겠다.
1. 오버엔지니어링
이번 프로젝트에서 "대규모 사용자가 존다한다면?" 가정을 적용해 AWS S3 스토리지를 최적화하고자 했다.
이 경험은 서비스워커를 사용하여 S3 스토리지 최적화하기 (그리고 오버엔지니어링에 대해) 글에서 소개했다.
해당 글에서 표현했듯이 문제를 해결한 후 오버엔지니어링을 했음을 느꼈다.
미래에 발생할 가능성이 희박한 문제에 현재의 자원을 소모하여 요구사항 개발에 차질이 생겼기 때문이다.
세 줄 독후감 서비스를 사용자들에게 빠르게 제공하기 위해서는 S3 스토리지를 최적화하는 것보다 주어진 요구사항 개발에 우선을 두었어야 했다.
S3 스토리지를 최적화할 필요도 있었나 싶다.
사용자의 실제 요구사항과 주어진 자원을 고려하는 적절한 논리력의 중요성을 뼈저리게 깨달았다.
2. 불필요한 리렌더링
아래는 실제 프로덕션 코드에 배포되어있는 컴포넌트의 일부다.
완료 버튼을 클릭할 시 props로 받은 onComplete 콜백 함수에 이름과 소개 입력 값을 전달해주고 있다.
...
const ProfileSettingModal = ({
...
initName = '',
initIntroduce = '',
onComplete,
...
}: ProfileSettingModalProps => {
const [name, setName] = useState<UserName>(initName);
const [introduce, setIntroduce] = useState<Introduce>(initIntroduce);
...
return (
...
<TextField
label="이름"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextArea
label="소개"
value={introduce}
onChange={(e) => setIntroduce(e.target.value)}
/>
<Button
...
onClick={() => onComplete({ name, introduce })}
>
완료
</Button>
...
);
};
export default ProfileSettingModal;
일반적으로 주로 사용하는 input의 value를 상태로 제어하는 제어 컴포넌트다.
따라서 이름 또는 소개의 변경이 일어날 때마다 상태가 변경되므로 리렌더링이 일어난다.
"리렌더링이 일어났기에 무조건 문제다!" 라는 것이 아니다.
여기서 중점은 "왜 제어 컴포넌트로 지정했는가?" 이다.
3. API 요청 관리에 대한 찜찜함
아래와 같이 API 요청 코드를 관리하고 있다.
export const getAllBookReviewId = async () => {
try {
const response = await get<HttpResponse<Pick<BookReviewResponse, 'id'>[]>>(
`${API_URL}/list/all`,
);
if (response.error) {
throw new BookReviewError({
name: 'GET_ALL_BOOKREVIEW_ID_ERROR',
message: response.message,
});
}
return response.data;
} catch (error) {
const { message } = getDataFromAxiosError(error);
throw new BookReviewError({
name: 'GET_ALL_BOOKREVIEW_ID_ERROR',
message,
});
}
};
겉으로 보기에 별 문제가 없어 보인다. 사실 그게 맞다. 코드 하나만 보여줬으니..
위 코드는 독후감에 대한 도메인과 관련이 있기 때문에 bookReview.ts 파일에 작성되어 있다.
그리고 bookReview.ts 파일에는 456 줄의 코드가 작성되어 있다.
이에 비해 다른 API 요청 파일 내 코드는 150 줄을 넘지 않는다.
이처럼 독후감 도메인에 많은 API 요청 로직이 집중되어 있고, 그에 따라 요청 함수가 많아진 것이다.
함수가 많아짐에 따라 매번 특정 함수 코드를 찾기도 까다로워졌고, 해당 함수에 따른 인터페이스가 늘어나 코드를 관리하기도 어려워졌다.
실제로 이미 지정한 인터페이스가 파일 내 있음에도 이름만 다른 중복 인터페이스를 생성하기도 했었다.
또한, 위 코드의 에러 처리 부문이 파일 내 모든 함수에서 반복되었다.
다른 문제를 살펴보자.
아래 코드는 특정 API 요청 함수 내 작성되어있는 자료구조다.
const publishRequest: PublishRequest = {
...bookReview,
id: bookReviewId,
bookname: bookReview.book.title,
authors: bookReview.book.authors.join(', '),
publication: bookReview.book.datetime.slice(0, 10),
publisher: bookReview.book.publisher,
thumbnail: bookReview.thumbnail || '',
originThumbnail: bookReview.book.thumbnail,
categoryId: bookReview.category.id,
tags: Array.from(bookReview.tag),
createdAt: bookReview.createdAt,
isDraftSave,
userId,
};
API 요청을 위해 데이터의 자료구조를 변환하는 작업인데, 문제는 API 요청 함수 내에 있는게 맞냐는 것이다.
위와 동일한 자료구조 변환 작업이 다른 API 요청 함수에 존재하는 경우가 꽤 있다.
이와 비슷한 맥락으로 API 요청 함수 내 비즈니스 로직까지 존재하기도 한다.
더 큰 문제는 응답 받은 데이터에 대해 자료구조 변환 및 비즈니스 로직의 반대 작업을 또 다시 시행해야 된다는 것이다.
4. 체감되는 느린 서버 사이드 렌더링
Next.js를 이용한 이유들 중 하나는 빠른 초기 로딩 속도였다.
하지만 브라우저가 서버에 HTML을 응답 받는 서버 사이드 렌더링 속도가 체감이 될 정도로 느리다.. 약 1.33초 정도가 걸린다.
현재 파악해 본 결과, 모든 것을 서버 사이드 렌더링하려 하는 것이 문제가 아닐까 생각한다.
초기 화면에 보이지 않는 자원, 검색 엔진 최적화에 영향을 미치지 않는 자원, 크기가 큰 이미지 목록 등 서버 사이드 렌더링이 불필요한 부분들에 대해 분석해봐야 겠다.
그리고 부분별 클라이언트 사이드 렌더링을 적용하여 서버 사이드 렌더링 속도를 개선해보고자 한다.
앞으로의 목표
- 스스로 피드백
서비스를 이용하는 사용자들에게 피드백을 받을 수도 있고, 내 스스로가 피드백을 할 수 있다.
하지만 전자는 그 경우가 극도로 적으니 지속적인 후자의 방법이 목표다.
여러 테크 블로그 및 팔로우 중인 현업의 개발자 분들의 문제 해결을 참고하고 내 것으로 만들어 지속적으로 학습해 나갈 것이다.
그리고 배운 것을 통해 기존의 내 코드와 아키텍처에 대해 피드백을 하고, 피드백에 대해 실제로 코드 개선을 진행해보며 성장을 위한 선순환을 만들어보자.
학습 → 피드백 → 코드 개선 → 학습 → 피드백 → ...
- 취업
취업을 하면 스스로의 더 가파른 성장이 기대된다.
훌륭한 팀원들과 빠르게 실패하고 그에 따른 피드백을 주고 받고 싶다.
그리고 내가 경험해보지 못한 무궁무진한 문제들을 풀어볼 생각에 설렌다.
하지만 여태껏 원하는 기업에 입사 지원하는 것에 자신감이 없었다.
"나는 아직 준비가 덜 됐어" 라는 말을 수도 없이 되뇌었다.
그러다 생각했다. "준비라는게 완벽히 될 수 있을까? 내가 생각하는 준비란 뭘까"
학습에 대해서는 "빠르게 실패하자" 라고 늘 되새기면서, 회사에 지원하는 것은 그러지 않고 있었다.
자신감을 가지자. 부딪혀보자.