
일을 시작하고 나서 바쁘다는 핑계로 벨로그, 공부 등등 하나 둘 놓다보니 어느 순간 두려움이 엄습해왔다. 빠르게 성장해서 스펙을 쌓아가고 싶은 마음은 굴뚝 같은데, 어디서부터 뭘 해야할지 막막했다. 그 와중에 감사하게도 지인분이 토스 모의고사 2회차 진행 소식을 알려주셨고, 링크를 전달받아 참여하게 되었다.
토스 모의고사에 참여하게 된 이유로 성장에 대한 병목을 해소하고 싶었던 것도 있지만, 막연하게 자리하고 있던 토스 과제에 대한 두려움을 떨쳐버리고 싶었던 이유도 있었다. 이 모든 것들을 걱정하면서도 아무것도 하지 않던 나태함을 어떻게든 극복하고자 참여했다.

과제의 내용은 생각보다 단순했다. 이미 기능이 동작하도록 구현되어있는 회의실 예약 페이지 코드를 리팩토링 하는 과제였다.
"서비스의 유지보수나 장기적인 확장성을 고려한 설계, 추상화 관점에 집중해서 기능을 구현해주세요."
그런데 생각보다 단순한 과제에 비해 나는 어떻게 시작해야할지 전혀 감이 안왔다... 이전에 어떤 코드를 체계적으로 리팩토링 해 본 경험이 없었을 뿐만 아니라 '토스 개발자들은 어떤 참신한 고민들을 할까..?' 싶은 생각에 나의 보잘 것 없어 보이는 고민들을 드러내는 것이 부끄러웠다.
그래서 일단 1회차 과제 제출물들을 살펴보고, 1회차 과제 후기들을 보며 공부부터 시작했다. 그 중 자주 나타났던 키워드는 추상화와 추출이었고, 이 두 개념을 구분하고 '추상화'가 무엇인지에 대해 고민을 해봤다. 자료들을 찾아가며 어떤 부분을 추상화할 수 있는 특별한 포인트가 없을지 고민해봤지만 큰 수확 없이 시간만 속절없이 흘러갔다. 아무래도 추상화에 대한 스스로의 명확한 기준이 없다보니 다 맞는 말처럼 느껴져서 방향성을 제대로 잡지 못 했던 것 같다.
정말 이러다가는 시간 내에 아무것도 못 할 것 같다는 생각이 들 때 쯤에 방향성을 변경했다. 우선 내가 기존에 알고 있던 '추상화'는 무엇일까 생각해보고 이를 토대로 리팩토링을 진행하기로 했다. 일단 뭐라도 해보고 회초리 맞으며 배워야겠다고 생각했다.
그렇게 크게 세 가지 기준에서 리팩토링을 진행했다.
- 확장성과 유지보수
- 인지 부하 해소
- 구현 세부사항 은닉
이 기준들과 함께 '읽기 쉬운 코드'를 작성하는 것을 목표로 작업을 시작했다.
우선은 폴더 구조를 최대한 단순하게 잡았다. 요즘 핫한 FSD 아키텍처와 관련해서는 깊이 있게 알고 있지도 않았고, 쓸데없이 화려한 느낌이 있어서 사용하지는 않았다.
RoomBookingPage/ # 회의실 예약 페이지
├── index.tsx
├── components/ # 이 페이지에서만 사용하는 컴포넌트
├── hooks/ # 이 페이지에서만 사용하는 훅
├── constants/ # 이 페이지에서만 사용하는 상수
└── utils/ # 이 페이지에서만 사용하는 순수 함수
각 페이지별로 기능 단위로 최소한의 폴더로만 구성했고, 공통 코드는 /shared 디렉토리로 분리했다.
export function ReservationStatusPage() {
...
const { data: rooms = [] } = useQuery(getRoomsQueryOptions());
const { data: reservations = [] } = useQuery(getReservationsQueryOptions(date));
const { data: myReservationList = [] } = useQuery(getMyReservationsQueryOptions());
...
const handleCancel = async (id: string) => {
try {
await cancelReservation(id);
setMessage({ type: 'success', text: '예약이 취소되었습니다.' });
} catch {
setMessage({ type: 'error', text: '취소에 실패했습니다.' });
}
};
return (
...
<Spacing size={24} />
<Border size={8} />
<Spacing size={24} />
{/* 예약 현황 타임라인 */}
<div css={css`padding: 0 24px;`}>
<Text typography="t5" fontWeight="bold" color={colors.grey900}>
예약 현황
</Text>
<Spacing size={16} />
<ReservationTimeline rooms={rooms} reservations={reservations} />
</div>
<Spacing size={24} />
<Border size={8} />
<Spacing size={24} />
...
);
}
컴포넌트 분리에 있어서는 최대한 페이지단에서 데이터 호출이나 핸들러 함수들을 관리하고, 하위 컴포넌트는 순수성을 유지하려고 했다. 그리고 그 과정에서 페이지 컴포넌트만 보더라도 데이터의 흐름이 한 눈에 파악될 수 있도록 인지 부하를 유발하는 요소들을 유틸 함수나 훅으로 분리했다.
// src/ReservationStatusPage/index.tsx
const { data: rooms = [] } = useQuery(getRoomsQueryOptions());
const { data: reservations = [] } = useQuery(getReservationsQueryOptions(date));
const { data: myReservationList = [] } = useQuery(getMyReservationsQueryOptions());
// src/shared/queries/reservation.ts
export const ROOMS_QUERY_KEY = ['rooms'] as const;
export const RESERVATIONS_QUERY_KEY = ['reservations'] as const;
export function getRoomsQueryOptions() {
return {
queryKey: ROOMS_QUERY_KEY,
queryFn: getRooms,
};
}
export function getReservationsQueryOptions(date: string) {
return {
queryKey: [...RESERVATIONS_QUERY_KEY, date] as const,
queryFn: () => getReservations(date),
enabled: !!date,
};
}
쿼리 옵션을 함수로 추상화하여 호출부에서는 queryKey와 queryFn의 세부사항을 몰라도 되도록 했다. 또한 유지보수성을 고려해 하드코딩된 쿼리키를 상수로 분리했다. 특히, RESERVATIONS_QUERY_KEY의 경우 예약 취소 로직에서 해당 키를 통해 invalidateQueries() 처리를 해줘야 했기 때문에 상수로 분리하는 것이 좋다고 판단했다.
크게 이 정도 틀에서 작업들을 수행했고, 어느덧 제출 마감시간이 되어 서둘러 마무리했다. 돌이켜보니 이것저것 공부했던 것에 비해서 너무 단순 코드 추출 작업만을 하다 과제를 마무리한 것 같아서 많은 아쉬움이 남았다. 다음번에도 기회가 된다면 좀 더 여유롭게 과제를 시작해서 깊이 있는 고민을 하고 다른 분들과 토론을 나눠볼 수 있으면 좋을 것 같다.
해설강의는 총 2부로 나누어 진행되었고, 오종택님이 진행을, 한재엽님과 문동욱님이 각각 1부와 2부에서 해설 및 코드 리뷰를 진행해 주셨다. 결론부터 얘기를 하자면, 해설강의를 듣고서 정말 많은 생각이 들었다. 특히, 나 스스로 '토스'에 대해 실체 없는 벽을 세워두고 있었다는 것을 깨달았다. '끊임없이 참신한 것들을 만들어 내는 곳', '토양어선이래...', '코딩 괴물들만 가는 곳' 등등...
해설 강의를 들으며 깨달은 것은 이번 모의고사에서 '얼마나 참신한 고민을 하는 것'은 전혀 중요한 것이 아니었다. '당연한 것을 당연하지 않게 생각하는 것'이 정말 중요하다는 것을 느꼈다. 코드를 작성함에 있어서 내 기준이 없었던 것은 '당연한 것을 너무도 당연하게 생각'하고, 어떤 선택지에서 뭐가 더 좋을지 고민하기 보단 '둘 다 괜찮은데?'라며 가벼이 넘겨 버렸기 때문이 아닐까 싶다. 내가 당연하게 넘겼던 그 사소한 부분들에서 디테일한 차이를 만들어 내고 그것들이 모여 참신함을 만들어내는 사람들이 모인 곳이 토스라는 생각이 들었다.
해설 강의 내용에 대해서는 간략하게 인상깊었던 부분 위주로 남겨 본다.
어찌보면 너무 당연한 말일수도 있는데, 이미 짜여진 코드를 리팩토링 하라고 했을 때 빈 파일부터 열었던 적은 단 한 번도 없는 것 같다. 재엽님의 해설 강의 시작은 빈 파일에서 이상적인 인터페이스를 설계하는 것부터 시작됐다. 이 과정이 기존 코드를 먼저 봤을 때 알게 모르게 생기는 미련(?)을 버리는 데 도움이 된다고 하셨다.
이 부분이 공감이 됐던 게 실제로 어떤 코드를 보면 '다시 짜는 게 낫겠는데?' 싶은 코드들이 있다. 보통 이 경우에 생각은 이렇게 하더라도 '이 복잡하고 지저분한 걸 어떻게 리팩토링 해?'를 돌려 말한 것이지 실제로 다시 짜려고 했던 생각은 아니었다... 이게 어찌보면 내 스스로가 리팩토링에 대해서 '기존 코드에서 시작한다'는 선입견을 가지고 있었던 것이 아닐까 하는 생각이 들었다.
그래서 이게 뭔데! 이제는 나도 좀 알자! 재엽님이 추상화와 추출의 차이에 대해서 예시를 들어 설명해주셨는데, 내가 이해한 바로 결론을 내려보자면, 추출은 '있는 그대로' 코드를 덜어내는 작업이고, 추상화는 코드를 덜어내면서 '최대한의 재사용성을 확보'하는 작업으로 이해했다.
예시는 회의실 예약 화면에서 몇 층의 회의실을 선택할 것인지 고르는 Select 컴포넌트를 예로 드셨다. 이 Select 컴포넌트는 단순히 1~6 사이의 숫자를 고르게 되어있다. 이 경우에 이 컴포넌트를 FloorSelect 컴포넌트로 분리한다면, 단순히 숫자를 고르는 컴포넌트임에도 불구하고 컴포넌트명으로 인해 층 수 선택에만 사용할 수 밖에 없게 된다. 이 때, NumberSelect로 분리하게 되면 컴포넌트의 기능에 핏(fit)한 컴포넌트명을 갖게 되면서 재사용성이 확보된다. 그렇다고 해서 무조건 FloorSelect로 구분하는 것이 잘못된 것은 아니다. 예를 들어 L(로비층), F(4층) 등 숫자가 아닌 선택지가 Select 리스트에 포함되는 경우에는 FloorSelect가 좋은 추상화가 될 수도 있다.
우리가 코드를 읽을 때 단순히 그 부분만을 읽는 것이 아니고, 뒤에 따라올 내용을 예측하며 읽게 된다. 그런데 예측이 빗나가게 되면, 다시 생각하게 되고, 그런 것들이 반복되면 이해하기 어려운 코드가 된다. 반대로, 예측하기 쉬운 코드가 이해하기 쉬운 코드가 되는 것이다.
동욱님이 몇 가지 예시를 들어 설명해주셨다. 예를 들어 컴포넌트 하단에 Spacing이 들어가 있는 경우 이 여백(Spacing)은 컴포넌트의 기능과 관련이 없다. 따라서 이런 코드들은 컴포넌트 외부에 위치하는 것이 적절하다.
또 함수(컴포넌트)의 인터페이스는 해당 함수의 연결부이다. 그래서 함수의 인자들은 내부를 바라보고 있어야 하고, 호출부의 맥락에 관심을 가지는 순간 재사용성이 현저하게 떨어지게 된다. 예를 들어서 날짜를 선택하는 기능이 있는 DataSelector 컴포넌트에서 setDate를 인자로 받아서 onChange에 전달 해주고 있을 때, 컴포넌트의 props로 setDate={setDate}로 받는 것보다는 onChange={setDate}로 받는 것이 더 적절하다.
한편 동욱님은 이렇게 해설 강의를 진행했지만, 이것이 완전한 정답은 아니며 항상 의심하고 다시 생각해볼 것을 당부하셨다.
항상 개발 과제들을 진행하면서 '나의 특별함을 어떻게 보여주지?'라는 생각을 했던 것 같습니다. 이번 과제 또한 처음에 '추상화'에 대해서 학습한 뒤, 남들이 발견 못 한 어떤 부분을 기가막힌 방법으로 추상화할 수 있지 않을까하는 생각으로 접근했던 것 같습니다.
하지만, 이번 과제를 통해 느낀점은 '추상화'라는 것은 특별하고 특출난 기법에 의미가 있는 것이 아니고, 지극히도 보편적인 사고에 기반하여, 모두가 공감할 수 있는, 어떤 가려운 부분을 긁어주는 것에 가까운 작업이라고 느꼈습니다. 그렇기 때문에 영원한 정답이 있는 것도 아니고, 앞으로 개발을 하면서 계속해서 제 스스로 이 기준을 만들고 수정해 나가야겠다고 생각했습니다.
마지막으로 좋은 기회를 마련해주신 종택님, 재엽님, 동욱님께 감사하다는 말씀 드리며 글을 마치겠습니다! 🙇