[회고] 우아한테크코스 Level 2 후반기 회고
객체지향의 진가를 체험하고, 풀스택 미션으로 시스템 전체를 설계하며, 함께 자라는 순환을 직접 겪은 레벨 2 후반기의 기록입니다.
로딩중...
오래 걸리지 않기를 바라요..!
객체지향의 진가를 체험하고, 풀스택 미션으로 시스템 전체를 설계하며, 함께 자라는 순환을 직접 겪은 레벨 2 후반기의 기록입니다.
이번 미션은 풀스택으로 진행됐고, 나는 express로 백엔드를 처음 제대로 공부하게 됐다. 사실 5년 전에 express 클론 코딩을 해본 적이 있는데, 그때는 정말 어렵게 했던 기억이 있다. 그런데 지금 와서 다시 해보니 잘 작성하는 것이 어려울 뿐이지 express 사용 자체는 너무 쉽게 느껴졌다. 공식 문서를 읽는 것도 수월했고, "이런 기능이나 솔루션도 있겠지" 하는 직감이 실제로 맞아떨어지는 경우도 많아, 그동안의 성장이 체감되기도 했다.
처음 백엔드 미션을 진행할 때에는 함수형으로 작성했었다. 개인적으로는 객체지향에는 솔직히 회의적인 입장이었다. SOLID를 지키면서 코드를 짜보니 객체가 정말 많이 생겼는데, 이 많은 객체를 어떻게 다 관리하는지 상상이 안 갔다. 그 수 많은 객체들을 원리에 의해 나눈다고 한들 그게 요구사항의 변경방식과 정확히 맞아떨어지지 않기에 오히려 여러 객체를 한번에 바꿔야 하는 경우가 생긴다고 생각했다. 게다가 코드의 흐름을 볼 때 각 객체의 명확한 역할을 인지 해야만 큰 틀에서 이게 어떤 방식으로 동작하는구나를 이해할 수 있어서 가독성적으로도 떨어진다고 생각하고 있었다. (사실 당시 내 객체지향 지식은 학교에서 배운 정도가 전부였기 때문에 할 수 있는 생각이긴 하다..!)
생각이 바뀐 계기는 시지프의 express를 사용해 의존성 주입 라이브 코딩 수업때였다. 계층을 어떻게 나누는지, 검증과 생성을 함께 두는 응집도 있는 코드가 무엇인지, 그리고 왜 타입스크립트 interface의 이름이 interface인지를 눈앞에서 보면서 엄청난 깨달음을 얻었다. "이게 객체지향의 맛이구나!!"
그 주 주말에는 크루들과 캠퍼스에 나와 수업 코드를 바탕으로 의존성 관계를 도식화하면서 인터페이스를 다시 이해했다.
인터페이스는 서로에 대한 규약이라서, 어느 카페에 가서 "커피 주세요" 해도 같은 결과를 받을 수 있다. 구현을 감추고 약속만 노출하기, 구현이 아니라 인터페이스에 의존하기, "tell, don't ask"까지. DIP, DI, OCP와 같은 지금것 몰랐던 객체지향의 진가를 체험해보았다.
수업을 듣고 또 미션을 진행하면서 아직까지 아키텍쳐에 대한 궁금증이 다 풀리지 않았었다. 이런 아키텍쳐는 어느정도 정답이 존재하고, 그게 Spring Boot나 Nest와 같은 프레임워크가 지정한 틀이라는 생각을 하게 되어 틈틈이 nest 공부를 하면서 미션을 다시 구현해봤다.
nest로 구현해본 레포(공부 내용 정리 문서) : geongyu09/-nest-shopping-cart-full-stack/docs
그러면서 라이프사이클이나 모듈 간 작업 처리 방식(CQRS 같은 것들)을 알게 됐다. 결과적으로 백엔드에 정말 큰 흥미가 생겼다. 객체지향도 너무 좋고, 앞으로도 관심을 가지고 쭉 공부해보고 싶다.
백엔드에서 경계를 나눠본 경험을 프론트로 가져와서, 미션 프론트 코드에도 경계 나누기를 시도했다. 우선 수업 때 내가 세운 기준은 두 가지였다. 모든 로직에 도메인을 분리할 것, 그리고 자주 변하는 것이 자주 변하지 않는 것에 의존하게 할 것.
컴포넌트와 훅, 그리고 일반 로직을 모두 도메인이 있느냐 없느냐를 첫 기준으로 삼아 나눴다. 도메인이 있는 컴포넌트는 feature로 묶고, 그 안을 다시 재사용 가능성에 따라 pages/screens와 widget으로 나눴다. 도메인이 없는 컴포넌트는 common으로 두고 내부 로직을 담는 entities와 순수한 UI를 담는 shared로 나눴으며, shared는 여기서 한 번 더 layout과 ui로 나눴다. 훅도 같은 방식으로 도메인 유무를 기준 삼아, useQuery나 useForm처럼 도메인과 무관한 것은 common에 두고 useUserAuth나 useLoginForm처럼 특정 도메인에 묶이는 것은 feature에 뒀다. 일반 로직 역시 isString이나 isInArange처럼 도메인과 상관없는 순수 함수는 utils에, checkIsValidUsername처럼 도메인 규칙이 담긴 함수는 libs에 두는 식으로 나눴다. 이를 다이어그램으로 그리면 아래와 같다
또한 백엔드에서 repository layer가 외부 연결처럼 교체가 잦은 부분을 격리해주는 것을 보면서, 프론트에서는 api(fetch) 로직이 같은 자리에 놓인다고 생각했다. 그래서 apis/ 레이어를 따로 분리하고 그 위에 커스텀 훅 레이어를 두어, 컴포넌트는 훅만 호출하게 만들었다.

(시간이 없어서 수업시간때에는 완벽하게 표현하지는 못했다..!) 백엔드에서 배운 계층과 의존성 감각이 프론트 구조 설계로 그대로 이어진 경험을 할 수 있었다.
미션에서 useQuery 훅을 직접 구현하면서 useSyncExternalStore를 사용했었다.
미션이 끝난 이후에 시지프가 이 훅을 사용한 크루 중에서 수업 시간에 간단히 useSyncExternalStore에 대해서 설명해줄 크루를 찾고 있었다.

다들 개념을 애매하게 알고 있다는 느낌이라 선뜻 나서는 사람이 없었다. 그래서 같이 공부해보는 시간을 가져보면 좋을 것 같아 슬랙에 남겼고, 총 4명(아지·레스·클라우디·파라디)이 신청해서 함께 공부했다. 방식은 원정대 활동과 같은 전투학습으로, 질문 4개를 뽑고 30분 동안 어떤 방식으로든 공부를 진행해서 각자 답을 채워가는 식이었다.

이렇게 함께 공부한 이후에는 나와 아지가 프론트 수업에서 발표를 진행했다. 발표 자체를 전혀 준비하지 않은 상태로 했더니 조금 더듬은 부분이 있었고, 듣는 사람마다 이해 수준이 다 달랐는데 한 번이라도 미리 생각했으면 더 포괄적인 발표를 만들 수 있었을 것 같아 아쉬움이 남았다.
그래도 뭔가 이런 함께 공부하는 기회를 만들지 않았다면 아마 이 훅에 대한 공부를 계속해서 미뤘을 텐데, 덕분에 많이 밀도 있게 공부하고 아웃풋까지 내본 경험을 할 수 있었다
마지막 프론트 수업 두 개의 주제는 좋은 코드에 대한 감각 익히기와 잘 설명하기였다. 먼저 예제 코드를 받고, 왜 냄새가 나는 코드인지 아는 단어로 최대한 표현해보는 활동을 했는데, 생각나는 게 결국 가독성이 떨어진다, 책임이 많다(SRP 위반) 정도였다..
좋은 코드에 대한 감각은 약간이나마 가지고 있지만, 그걸 말로 설명하고 그 근거를 대는게 너무 어렵다는 걸 느꼈다. 수업을 하다 보니 인터페이스 분리 원칙, 캡슐화 등등 정말 많은 용어와 법칙이 있었고, 이런 것들을 배워야겠다는 마음이 강하게 들었다.
좋은 코드에 대한 감각을 기르기 위한 실습으로는 지속적으로 변경되는 UI 요구사항이 주어졌고, 나는 피트와 페어로 진행했다. UI 컴포넌트를 크게 레이아웃과 UI로 나누는 내 기준을 설명했는데, 생각보다 많은 요구사항에 대응이 되는 좋은 코드였다고 생각한다. 방향성 자체는 어느 정도 괜찮았다는 느낌을 받았다. 하지만 "방향성이 어느 정도 괜찮네" 정도로 끝내기에는 너무 아쉬웠다. 그래서 2차 워킹그룹을 열어, 변경에 유연한 컴포넌트를 어떻게 설계할 수 있을지 다양한 크루들의 의견을 듣고 서로 피드백하면서 기준을 세워보기로 했다.

그렇게 열게 된 워킹그룹에서 서로 이야기하다 보니 내 논리가 반박 당하기도, 그리고 정말 다양한 주장들을 들어볼 수 있었다. 혼자 정리했으면 그냥 넘어갔을 부분이 부딪히면서 드러났고, 레이아웃 컴포넌트를 언제 어떻게 나누는 게 맞을지 나름의 결론까지 갈 수 있었다.
일단 레이아웃을 나누는 것 자체는 맞다고 봤다. 그래야 요구사항이 들어왔을 때 코드의 수정이 아니라 교체가 이뤄질 수 있기 때문이다.
interface Props {
img;
name;
id;
follower;
last;
}
function PersonProfileCard(props: Props) {
<ProfileLayout
image={<Image />}
name={<Text size="lg" />}
id={<Text size="sm" />}
desc={<Text size="sm" />}
rightContent={<Activity last={} />}
/>
}만약 이미지의 모양을 사각형으로 바꿔야 한다면 어떻게 될까? 레이아웃이 분리되어 있으면 슬롯에 꽂는 컴포넌트만 갈아 끼우면 된다.
function PersonProfileCard(props: Props) {
<ProfileLayout
image={<Rec.Image />}
name={<Text size="lg" />}
id={<Text size="sm" />}
desc={<Text size="sm" />}
rightContent={<Activity last={} />}
/>
}다만 많은 크루들이 이런 큰 레이아웃을 그렇게 긍정적으로 평가하지는 않았다. 너무 의미 없이 버려지는 것 아니냐는 이야기가 나왔고, 대신 조금 더 범용적으로 나눈 것들을 조합하는 방식은 괜찮다는 평이었다. 작게는 Flex, Stack 같은 것들, 크게는 SplitView 같은 것들 말이다. 이 이야기를 들으면서 레이아웃 또한 도메인이 있는 것과 없는 것으로 나눌 수 있겠다는 생각이 들었다.

레벨2의 마지막 미션은 장바구니/상품목록 풀스택 미션이었다. Node.js와 Express로 백엔드까지 직접 만들어 FE부터 BE까지 전체 시스템을 설계하고 구현하는 미션이다. FE 개발자도 혼자 돌아가는 서비스를 만들 수 있어야 전체 시스템을 설계하고 BE와 더 나은 협업 결정을 내릴 수 있다는 취지의 미션이지 않았을까 싶다.
1단계는 페어와 함께 백엔드 서버를 만드는 단계였다. 백엔드 경험이 없어 막막했던 터라 페어와 REST 원칙을 공부해서 지킬 규칙만 먼저 추렸다. 소문자와 하이픈을 쓰고, 복수형 명사를 쓰고, 계층 관계를 URL로 표현하고, 필터와 정렬은 쿼리 파라미터로 받기. 전부 알고 시작한 게 아니라 기준 몇 개만 세우고 부딪힌 셈이다. 결과물로는 Controller → Service → Repository → in-memory DB로 계층을 분리하고, 타입 가드로 스키마를 검증하고, 전역 에러 코드와 에러 처리 미들웨어를 뒀다. Jest와 supertest로 TDD를 하면서 render.com에 배포까지 했다.
만드는 과정에서 응답 형식을 정해야 했는데, 성공과 에러 모두 {status, message, data}로 통일했다. status code만으로는 비즈니스적으로 성공했는지, 에러의 원인이 무엇인지 구분하기 어렵다고 판단했기 때문이다. DELETE나 POST에도 204나 빈 응답 대신 추가·제거된 id를 내려주도록 했다. 그래야 프론트가 특정 상황에서 재요청 없이 캐시 갱신 같은 후속 처리를 할 수 있다고 생각했다.
이런 결정들은 프론트를 해봤기 때문에 내릴 수 있었던 결정이라고 생각한다. 이렇게 더 나은 설계를 하려면 폭넓은 경험이 필요하다는 걸 느낄 수 있었다!
상품 삭제 시 장바구니에 담긴 상품 또한 제거되어야 하는 요구사항이 있었다. 코드를 작성할 때 product와 cart로 모듈을 나누었고, 이 요구사항으로 인해 모듈의 규칙이 여기에서 섞이게 됐다. 비즈니스 규칙이라 repository 레벨에서 처리하는 건 부적절하다고 봤고 product service에서 cart repository를 호출하는 것으로 당시에 결론 냈지만, 올바른 결정인지는 감이 오지 않아서, (위에서 이야기 했듯) 직접 nest.js를 공부하면서 그 구조를 익히고자 했다. 결론적으로 서비스에서 서비스를 부르는 방식이 올바르다는 것을 알게됐다. 레포지토리 레이어는 너무나도 수정이 많이 되는 레이어이고, 이로 인해서 서비스 레이어까지 거쳐야만 코드의 변경이 전이되지 않는다는점을 알게됐다. 부가적으로 이렇게 서비스끼리의 의존성이 많이 생기게 되면 상호 의존성이 생길 수 있고, 이것은 설계적으로 다시 한번 생각해봐야한다는 신호라는 것도 알게됐다. 이때는 CQRS와 같은 통신 방식으로 해결할 수 있다는 것도 알게됐다.
2단계는 익숙한 프론트로 돌아와 장바구니 화면을 만드는 단계였다. 구조는 1단계 백엔드의 레이어드 아키텍처를 React로 가져와 common(UI)과 feature(도메인)를 분리하고 common 내부를 다시 entities와 shared로 나눴다. 실제로 나눈 구조와 그 이유는 이 정도고 일반화한 기준 이야기는 위의 [프론트에서 경계 나누기]에 적었다.
Suspense는 직접 구현했는데, 매 호출마다 throw Promise와 throw error를 손으로 쓸 수는 없어서 useSuspenseQuery로 추상화했다. 같은 query가 동일한 데이터를 공유하도록 싱글톤 store(구독/발행)를 두고 useSyncExternalStore로 스냅샷이 변할 때 재렌더되게 했다. queryCache로 중복 fetch를 막고 errorCache를 공유해서 throw하면 ErrorBoundary가 잡는다. 만들고 나서야 TanStack Query가 왜 이런 인터페이스로 만들어졌는지 어느 정도 알 수 있었다.
3단계는 코드를 한 줄도 쓰지 않는 전체 시스템 설계 미션이었는데, 처음엔 정말 낯설었다. 쿠폰과 배송비 정책이 새 요구사항으로 추가된 상황에서, 구현 전에 요구사항 문서, API 명세와 설계 의도, 시스템 설계, 그리고 User·FE·BE 시퀀스 다이어그램까지 4종 산출물로 책임 경계를 그렸다. 이 단계에서는 전부 반응성과 신뢰성과 같은 설계별 장단점의 트레이드오프를 고민하는 것이 주를 이뤘다.
계산에 대한 요구사항이 있었고, 나는 모든 금액 계산의 책임은 BE에 뒀다. 금액은 절대 틀리면 안 되고 정확성은 믿을 수 있는 정보 접근과 정확한 계산에서 나와야 한다고 생각했다. 가장 믿을 수 있는 원본은 클라이언트처럼 임의로 조작할 수 없는 DB이고 계산식도 한 곳에만 둬야 확장하거나 변경할 때 어긋나지 않는다. 하지만 BE에 그 책임을 둔 만큼 네트워크 왕복만큼 반응성(UX)을 포기해야만 했다. 그럼에도 나는 계산 오류인한 서비스적인 손해가 반응성으로 잃는 UX적인 손해보다 크다고 봤다. 쿠폰 할인 계산도 같은 이유로 BE에 뒀다. 쿠폰은 종류가 예측 불가능하게 발행될 수 있어서 프론트에 규칙을 두면 쿠폰이 추가될 때마다 FE와 BE를 동시에 수정해야 하기 때문이다.
이번 미션을 진행하면서 서비스 전체를 바라볼 수 있는 눈이 길러졌다. 설계라는 것은 절대로 완벽할 수 없다. 설계라는 것은 결국트레이드오프의 산물이다. 좋은 설계는 오늘의 요구사항을 완전히 만족하고 내일의 변경사항을 쉽게 반영할 수 있어야 한다. 이런 좋은 설계를 하기 위해서는 당연하게도 폭넓은 경험이 필요하며, 분야에 얽매이지 않고 많은 경험을 해보기 위해 노력해야겠다는 생각을 가지게 됐다.
이제 나는 깨닫거나 쌓은 것을 나만 가지고 있으면 안 된다고 생각한다. 혼자 품은 지식은 맞는지 틀린지 확인할 길이 없고, 밖으로 꺼내 나눠야 내가 몰랐던 관점이 들어오고 내 논리의 빈 곳이 드러나기 때문이다. 또 내가 하나를 풀어놓으면 다른 사람도 그걸 딛고 자기 것을 풀게 되어 서로가 서로를 자라게 한다. 이번 레벨2 후반기는 이런 생각을 여러 번 확인하고 깨닫게 해준 시간이었다.
우테코 level2 전반기 회고 : [회고] 우아한테크코스 Level 2 1~4주차 회고
처음부터 지식 공유가 매끄럽게 진행됐던 것은 아니었다. 레벨2 초에 리액트 스터디를 열었을 때는 범위를 정해 각자 공부하고 만나서 릴레이로 발표하는, 밖에서 여러 번 다듬어온 운영 모델을 그대로 가져왔는데도 돌아가지 않았다. 왜 안 됐는지 돌이켜보니, 크루들은 목표를 정해 파고드는 전투학습에 익숙했던 반면 나는 정해진 순서와 자료를 그대로 따라가는 학습에 익숙했고, 그 차이 탓에 기존 스터디 모델이 제대로 돌아가지 않았던 것이다. 그래서 이 모델을 전투학습에 맞게 어떻게 바꿔야 할지 고민해야 했다.
그렇게 만들어진 것이 정해진 시간 동안의 전투학습이었다. 리액트 원정대에서는 딱 하나의 질문에 답하기 위한 공부만 했고, 순서도 방법도 정하지 않은 채 공부했다. 생각보다 모두의 만족도가 높아서 무척 기뻤다. 또 이렇게 함께 활동하니 서로 지식의 빈 곳을 채워주게 되어, 혼자였으면 도달하기 어려웠을 깊이까지 파고들 수 있었다. 이런 장을 더 많이 열어봐야겠다고 다짐했다.
여러 시도를 했고, 물론 모두 성공적이지는 않았다.

Suspense 공유회는 준비한 자료 없이 시작했다가 인원이 많아지면서 단방향 느낌의 강의가 되어버렸다.
이런 방식으로 흘러갈 것이라 예상하지 못해 준비를 못 했고, 특히 청자를 생각하지 않은 채 발표를 시작한 탓에 내가 말하고자 하는 바를 잘 전달하지 못했다고 스스로 생각했다. 개인적으로는 공유회를 정말 망쳤다고 여겼는데, 반대로 크루들은 나에게 정말 도움이 됐다고 말해줬다. 이 덕분에 내가 잘 알고 잘 전하는 것과는 별개로 나누는 것 자체가 큰 가치를 만든다는 걸 체감했고, 틀리는 것도 잘하는 것도 그렇게 중요하지 않다는 걸 느꼈다. 함께 성장하려는 마음, 그리고 틀려도 잘하지 못해도 괜찮다는 심리적 안정감이 성장에서 가장 중요하다는 것을 깨달았다.
그때부터는 준비가 되어 있지 않아도 그냥 함께 편하게 이야기하는 자리 자체를 만들어보려 했다. 그렇게 연 것이 두 번째 워킹그룹이었다. 역시 많은 사람들이 관심을 가져줬고, 고맙기도 하면서 이런 자리가 결국 함께 성장하는 분위기를 만든다는 걸 알게 됐다. 이런 자리들이 가능했던 건 망한 발표에도 도움이 됐다고 말해주는 크루들 곁에서, 모르는 걸 드러내고 실수해도 괜찮다고 느꼈기 때문이라고 생각한다. "잘해야만 한다" 는 걱정에서 벗어나니 "자라는 데 집중" 할 수 있었다.
이제는 전혀 준비되지 않은 미숙한 생각이라도 꺼내놓는 일이 두렵지 않다. 내가 하나를 꺼내면 다른 크루도 그걸 딛고 자기 것을 꺼내면서, 혼자서는 도달하지 못했을 깊이까지 함께 가는 순환을 직접 겪어봤기 때문이다. 이번 레벨2에서 내가 가장 크게 얻은 것은 이 순환을 직접 겪어봤다는 것, 그리고 함께 자라기 위해 어떤 마음을 가져야 하는지 알게 됐다는 것이다.