토스 프론트엔드 모의고사 2회 후기
모의고사 1회 때는 선착순 안에 들지 못했다. 그래서 2회 모집 글이 올라왔을 때는 고민하지 않고 바로 신청했다.
요즘 회사에서는 빠르게 기능을 만드는 데 집중하고 있다. 그러다 보니 코드 퀄리티를 천천히 돌아볼 시간이 부족하다는 아쉬움이 있었다. 그래서 이번 모의고사는 단순히 문제를 푸는 자리가 아니라 내가 생각하는 좋은 코드의 기준을 다시 점검해볼 기회라 느껴졌다.
참여하면서 역시 가장 궁금했던 건 하나였다.
토스 개발자들은 좋은 코드를 어떤 기준으로 바라볼까?
실제로 참여해보니 그 질문에 대한 답이 조금 더 선명해졌다. 직접 과제를 하고 해설 방송을 들으면서 나 역시 좋은 코드를 볼 때 무엇을 중요하게 생각하는지 다시 정리할 수 있었다.
내가 먼저 세운 기준
이번 과제는 단순 구현 문제가 아니었다.
서비스의 유지보수나 장기적인 확장성을 고려한 설계, 추상화 관점에 집중해서 주어진 요구사항에 맞게 코드를 리팩토링 하라
회의실 예약 기능 요구사항과 코드 초안이 주어지고 위의 내용을 고려해 리팩토링 하라는 문제였다. 그래서 처음 코드를 봤을 때도 바로 먼저 어떤 기준으로 이 코드를 읽을지부터 정리할 필요가 있었다.
요구사항에 적힌 유지보수, 확장성, 추상화에 대해 먼저 내 생각을 정리해봤다.
1. 유지보수성
내가 생각하는 유지보수성은 문제가 생겼을 때 원인을 빠르게 좁혀갈 수 있는 구조다.
운영 중 장애가 나거나 개발 중 버그가 생겼을 때 어디를 봐야 하는지가 비교적 분명해야 한다. 동시에 장애 가능성이 높은 지점을 줄여서 이후 수정 비용도 낮출 수 있어야 한다고 생각했다.
2. 확장성
확장성은 요구사항이 늘어났을 때 적은 수정으로 대응할 수 있어야 한다.
예를 들어 필터 조건이 더 추가되거나, 시간 정책이 바뀌거나, 다른 화면에서도 비슷한 로직이 필요해졌을 때 변경 포인트를 쉽게 찾고 작은 수정으로 확장할 수 있어야 한다고 봤다.
3. 추상화
추상화는 세부 구현을 모두 알지 못해도 흐름을 읽을 수 있게 만드는 것이다.
페이지 컴포넌트를 위에서 아래로 읽을 때 지금 당장 알 필요 없는 정책과 구현이 너무 많이 드러나면 읽는 흐름이 끊긴다. 반대로 이름만으로 역할을 어느정도 파악할 수 있으면 전체 구조를 훨씬 쉽게 따라갈 수 있다.
물론 이 셋은 따로 떨어진 기준은 아니다. 적절한 추상화는 변경 포인트를 더 잘 드러내고 그 구조는 결국 유지보수성과 확장성으로 이어진다. 결국 이 개념들은 서로 깊이 엮여있다.
이 기준을 세운 뒤에는 아래 세 가지를 중심으로 코드를 보기 시작했다.
- 메인 페이지를 위에서 아래로 읽을 때 맥락이 과하게 드러나는 부분이 있는지
- 요구사항이 늘어났을 때 쉽게 깨질 것 같은 부분이 어디인지
- 다른 화면에서도 반복해서 사용할 수 있는 공통 로직이 있는지
그래서 실제로 무엇을 고치고 싶었나
searchParams를 상태처럼 다루는 흐름
가장 먼저 눈에 들어온 건 searchParams 기반 상태 관리였다.
URL에 이미 필터 값이 들어 있는데도 컴포넌트 안에서 그 값을 다시 꺼내 별도의 state로 만들고 값이 바뀌면 다시 URL을 갱신한 뒤 useEffect로 동기화하는 흐름이 반복되고 있었다.
동작은 하지만 읽는 사람 입장에서는 상태의 실제 출처가 URL인지 아니면 컴포넌트 state인지 한 번 더 확인해야 했다. 결국 같은 정보를 두 군데에서 관리하면서 흐름만 복잡해진 구조처럼 보였다.
그래서 이 부분은 URL 값을 직접 상태처럼 다루는 편이 더 자연스럽겠다고 생각했다. searchParams를 기준으로 값을 파싱하고 업데이트가 필요하면 유틸을 통해 다시 URL에 반영하는 식이다.
대략 이런 형태를 떠올리고 있었다.
function useRoomFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const filters = parseRoomFilters(searchParams);
const updateRoomFilters = (changed: Partial<RoomFilters>) => {
setSearchParams((prev) => updateRoomFiltersParam(prev, changed));
};
return {
filters,
updateRoomFilters,
};
}핵심은 searchParams를 한 번 더 복제한 상태를 만드는 대신 URL을 기준으로 읽고 업데이트하는 흐름으로 단순화하는 것이었다.
런타임 검증을 초기에 모으기
다음으로 눈에 들어온 건 런타임 검증이었다.
searchParams는 문자열 기반이라 잘못된 값이 들어와도 그대로 통과하기 쉽다. 날짜 자리에 엉뚱한 문자열이 들어오거나 숫자여야 할 값이 이상한 형태로 들어와도 지금 구조에서는 별다른 처리를 할 수 없었다.
그래서 문제를 뒤늦게 발견하는 구조보다는 초기에 파싱과 검증을 함께 처리하는 편이 더 낫다고 봤다.
export const roomFiltersSchema = z.object({
date: z.iso.date().default(formatDate(new Date())),
startTime: z.string().default(''),
endTime: z.string().default(''),
attendees: z.coerce.number().int().min(1).default(1),
equipment: z.array(z.enum(ALL_EQUIPMENT)).default([]),
preferredFloor: z.coerce.number().int().nullable().default(null),
});이렇게 두면 파싱 로직, 기본값 정책, 타입 검증이 흩어지지 않고 한곳에 모인다. 나중에 다시 코드를 읽는 사람도 훨씬 쉽게 맥락을 따라갈 수 있다.
변경 포인트를 한곳으로 모으기
또 하나 크게 보였던 건 변경 포인트가 흩어져 있는 구조였다.
equipment 타입, query key, 시간 슬롯 정책처럼 앞으로 바뀔 가능성이 높은 값들이 여기저기 흩어져 있으면 요구사항이 추가됐을 때 어디를 고쳐야 하는지 찾는 것부터 비용이 커진다.
장비 항목이 늘어나거나 시간 단위가 바뀌면 여러 파일을 오가며 같은 맥락의 값을 수정해야 하는 구조였기 때문이다.
돌아보면 내가 손보고 싶었던 건 개별 구현 하나하나보다 구조와 흐름이었다. 상태의 출처는 가능하면 줄이고, 잘못된 값은 초기에 걸러내고, 변경 가능성이 큰 포인트는 한곳에 모으고, 이름만으로도 역할이 드러나게 만들고 싶었다.
결국 내가 정리하고 싶었던 건 읽는 사람과 나중에 수정할 사람이 덜 헤매는 코드였다.
해설을 들으면서 다시 보게 된 것들
1부는 토스의 한재엽님께서 시작해주셨다.
해설에서 가장 인상 깊었던 건 내가 예상한 출발점과는 조금 달랐다는 점이었다.
나는 비즈니스 로직이나 구현 세부 이야기부터 시작할 줄 알았는데 실제 재엽님은 오히려 화면을 보고 이상적인 인터페이스를 먼저 그려보는 데서 출발했다.
UI와 코드가 1:1로 대응되는 구조
인터페이스를 짤 때 UI와 코드가 1:1로 대응되는 구조를 먼저 상상해보면 좋다고 하셨다.
기존 코드의 모양을 기준으로 조금씩 고쳐 나가기보다 이 화면이 가장 자연스럽게 읽히려면 어떤 단위로 나뉘어야 하는지를 처음 보는 시선에서 다시 그려보는 접근이었다.
예를 들면 이런 느낌이다.
export function RoomBookingPage() {
const [date, setDate] = useDate();
return (
<section>
<h1>회의실 예약</h1>
<section>
<h2>날짜 선택</h2>
<DatePicker value={date} onChange={setDate} />
</section>
<hr />
<section>
<h2>예약 현황</h2>
<ReservationStatusSection />
</section>
<hr />
<section>
<Flex gap={4}>
<h2>내 예약</h2>
<Text>{myReservationList.length}건</Text>
</Flex>
<Card top="토스뱅크" bottom="10:00 ~ 11:00" right={<Card.Button />} />
</section>
<FixedBottomCTA onClick={() => navigate('/booking')}>
예약하기
</FixedBottomCTA>
</section>
);
}사실 나는 이런 식의 컴포넌트 구성이 익숙하지 않아서 새로웠고 이렇게 구성했을 때 코드를 읽을 때 화면 흐름과 비슷한 레벨로 드러나 코드를 찾거나 이해하는데 확실히 좋겠다고 느꼈다.
사람은 코드를 읽는 것이 아니라 예측한다
2부에서는 문동욱님께서 이어서 해설을 진행했다.
사람은 코드를 읽는 것이 아니라 예측한다.
이 문장을 듣고 나서 내가 왜 어떤 코드를 읽기 어렵다고 느끼는지가 더 분명해졌다.
코드를 읽는 사람은 모든 줄을 처음부터 끝까지 해석하기보다 이름과 구조를 보고 다음에 무엇이 나올지를 먼저 예상한다. 그런데 인터페이스가 호출하는 쪽의 맥락을 너무 많이 품고 있거나, 상태의 출처가 여러 군데 흩어져 있거나, 컴포넌트 이름이 역할을 충분히 드러내지 못하면 그 예측이 자꾸 깨진다.
결국 읽기 어려운 코드란 예측이 계속 빗나가는 코드에 더 가깝다는 것이다.
예를 들어 컴포넌트 인터페이스를 설계할 때도 이런 차이가 있다.
// before
<DatePicker date={date} setDate={setDate} />
// after
<DatePicker value={date} onChange={handleDateChange} />후자가 더 자연스럽게 읽히는 이유는 컴포넌트가 알아야 할 바깥 맥락을 덜 드러내면서도 역할은 더 분명하게 보여주기 때문이다. 이 부분에 대해 이야기 해주시면서 HTML의 인터페이스를 따라가는 게 이상적이라고도 조언해주셨다.
정답보다 근거가 더 중요하다는 것
마지막에 동욱님께서 말해주신 건 좋은 코드에 정답은 없다는 것.
코드에 대한 설명에 충분한 논리와 근거를 댈 수 있다면 하나의 정답만 있는 건 아니고 해설 역시 그대로 외워야 할 답안이 아니라 사고를 확장하는 재료로 받아들여야 한다는 말이 특히 좋았다.
그 말을 들으면서 개발자에게 중요한 건 자신의 선택을 논리적으로 설명하고 비판적으로 돌아볼 수 있는 힘이라는 생각이 더 강해졌다.
참여자분들과 리뷰
다음 날 참여자 3분과 음성 채널에서 리팩토링한 내용과 해설 방송을 듣고 든 생각을 공유하는 시간을 가졌다. 내가 이해한 내용이나 질문에 대해서 다른 분들의 생각을 들을 수 있었다.
UI와 코드 1:1 매칭이라는 표현이 실제로 어디까지를 뜻하는지 제목까지 한 컴포넌트로 감추면 왜 오히려 흐름이 흐려지는지 같은 이야기를 더 구체적으로 나눌 수 있었다.
나는 useRoomFilters를 만드는 데 꽤 시간을 썼는데, nuqs처럼 searchParams 관리를 더 잘 도와주는 도구가 있다는 것도 그때 처음 알게 됐다.
좋은 코드에 대해 이야기 해볼 시간이 많이 없었는데 회사 밖의 분들과 코드에 대해 토론해볼 수 있어서 정말 좋았다.
느낀점
다시 한 번 느낀 건 코드를 작성할 때도 리뷰를 할 때도 왜 이렇게 작성했는가? 를 더 분명하게 설명할 수 있어야 한다는 것이었다.
예전에도 막연히 좋은 코드, 읽기 쉬운 코드를 지향한다고 생각했지만 이번에는 그 기준을 조금 더 구체적인 언어나 기준을 정리해볼 수 있었다.
추상화에 대해서도 생각이 조금 달라졌다. 전에는 분리하거나 감추는 것 자체가 추상화라고 막연히 생각한 적도 있었는데 지금은 무엇을 보여주고 무엇을 숨길지 선택하는 일이 더 중요하다고 느낀다. 읽는 사람이 지금 알아야 할 정보는 자연스럽게 보이고 지금 몰라도 되는 세부 구현은 뒤로 밀려나 있어야 흐름이 깨지지 않는다.
이번 토스 모의고사는 좋은 코드에 대해 스스로 다시 생각해보고 해설 방송을 들으면서 내 관점을 더 확장해볼 수 있는 계기였다. 빠르게 기능을 만드는 것만큼이나 왜 이렇게 작성했는지 설명할 수 있는 코드를 만드는 개발자가 되고 싶다는 생각이 들었다.