Activities2026년 3월 29일6 min read

토스 Frontend Fundamentals 모의고사 2회차 후기

회의실 예약 시스템을 리팩토링 해보기

토스 Frontend Fundamentals 모의고사 2회차 후기

토스 Frontend Fundamentals 모의고사 2회차에 참여하면서 경험한 것들과 후기를 정리해 보려 합니다.

직접 작업해보기

이번 리팩토링에서 다룬 화면은 두 개였습니다.

  • ReservationStatusPage: 날짜별 예약 현황을 보고, 내 예약을 확인하고, 예약 취소나 예약하기로 이동하는 화면
  • RoomBookingPage: 예약 조건을 입력하고, 가능한 회의실을 탐색한 뒤 실제 예약을 생성하는 화면

이 두 화면은 분리된 페이지이지만, 사용자 입장에서는 하나의 흐름입니다.

다이어그램을 준비하고 있습니다.

1. 페이지 컴포넌트는 조합(Composition)에 집중

리팩토링 전에는 두 페이지 모두 화면 렌더링, 상태 관리, 검증, API 호출, 메시지 처리까지 한 파일에 모여 있었습니다. 이 상태에서는 기능은 동작해도, 수정하려면 너무 많은 문맥을 한 번에 읽어야 했습니다.

그래서 페이지는 최대한 조립만 담당하도록 바꿨습니다.

export function RoomBookingPage() {
  return (
    <PageLayout title="예약하기">
      <ReservationSearchForm />
      <AvailableRoomList />
      <BottomFloat>
        <Button display="full">확정</Button>
      </BottomFloat>
    </PageLayout>
  );
}

이후 검색 조건, 선택 상태, 예약 생성, 메시지 처리 같은 세부 책임은 훅과 컴포넌트 바깥으로 분리했습니다. 그 결과 페이지는 “무엇을 보여주는지”가 먼저 읽히고, 세부 구현은 필요할 때만 따라가면 되는 구조가 됐습니다.

2. 한 곳에서 많은 책임을 담고 있는 상태라 분리하기

특히 RoomBookingPage는 원래 한 파일 안에서 너무 많은 일을 하고 있었습니다.

export function RoomBookingPage() {
  // 검색 파라미터 동기화
  // 입력 상태 관리
  // 검증
  // 예약 가능 회의실 계산
  // 예약 생성 mutation
  // 에러 처리
  return <Page />;
}

이를 성격별로 나누었습니다.

  • 검색 조건 상태: 날짜, 시간, 참석 인원, 장비, 층
  • 선택 상태: selectedRoomId
  • 화면 메시지 상태: 성공/실패 배너
  • 서버 상태: rooms, reservations, myReservations

예를 들어 검색 조건은 전용 훅(useReservationSearchFilters)으로 분리했습니다.

function useReservationSearchFilters() {
  const [searchParams, setSearchParams] = useSearchParams();
 
  const [date, setDate] = useState(searchParams.get("date") ?? today());
  const [startTime, setStartTime] = useState(searchParams.get("startTime") ?? "");
  const [endTime, setEndTime] = useState(searchParams.get("endTime") ?? "");
  const [attendees, setAttendees] = useState(Number(searchParams.get("attendees")) || 1);
 
  useEffect(() => {
    setSearchParams({
      date,
      startTime,
      endTime,
      attendees: String(attendees),
    });
  }, [date, startTime, endTime, attendees, setSearchParams]);
 
  return { date, startTime, endTime, attendees, setDate, setStartTime, setEndTime, setAttendees };
}

3. 검증과 비즈니스 로직을 JSX 밖으로 이동

이번 리팩토링에서 가장 중요했던 포인트 중 하나는, 비즈니스 로직이 화면 코드 안에 섞이지 않게 하는것이었습니다.

특히 RoomBookingPage의 핵심은 “조건에 맞는 회의실을 찾는 것”인데요. 그래서 층, 인원, 장비, 시간 충돌 조건을 JSX 안에서 직접 처리하지 않고, 이름이 있는 규칙으로 분리하였습니다.

const byMinCapacity = (minCapacity: number) => (room: Room) => room.capacity >= minCapacity;
 
const byEquipment = (required: Equipment[]) => (room: Room) =>
  required.every((equipment) => room.equipment.includes(equipment));
 
const combineFilters =
  (...filters: Array<(room: Room) => boolean>) =>
  (room: Room) =>
    filters.every((filter) => filter(room));

이후에는 페이지에서 아래처럼 읽히게 만들었습니다.

const roomFilter = combineFilters(
  byMinCapacity(attendees),
  byEquipment(equipment),
  byAvailableTime(reservations, startTime, endTime),
);
 
const availableRooms = rooms.filter(roomFilter);

단순히 코드가 짧아진 것보다, 왜 이 로직이 존재하는지가 더 잘 드러나도록 작업했습니다.

4. 데이터 패칭도 페이지 책임에서 분리

두 화면 모두 서버 데이터에 의존하고 있습니다.

  • ReservationStatusPage: 회의실 목록, 날짜별 예약 목록, 내 예약 목록
  • RoomBookingPage: 회의실 목록, 예약 현황, 예약 생성 요청

이 데이터를 페이지에서 직접 관리하기보다, query와 service 계층으로 분리했습니다. 이렇게 하면 페이지는 “어떤 데이터를 필요로 하는지”만 알고, 캐시 키나 요청 세부사항은 밖에서 관리할 수 있습니다.

5. 재사용 UI 공통화

두 화면을 함께 정리하면서 아래 컴포넌트들을 공통화했습니다.

  • PageLayout: 페이지 공통 여백과 제목 패턴
  • Section: 라벨이 있는 콘텐츠 블록
  • DateSelector: 날짜 선택 input
  • TimeSelector: 시간 선택 옵션 묶음
  • MessageBanner: 성공/실패 메시지
  • MyReservationSection: 내 예약 목록과 취소 액션

이 컴포넌트들의 가치는 재사용성보다, 화면이 같은 문법으로 읽히게 만든다는 데 있었습니다. 예를 들어 Section이라는 이름이 붙는 순간 그 블록은 단순한 div가 아니라, 하나의 주제를 가진 화면 단위가 됩니다.

결국 리팩토링은 컴포넌트를 많이 만드는 작업이 아니라, 화면의 의미를 코드에 남기는 작업에 더 가까웠다고 볼 수 있습니다.


얻은 것들

아래는 리뷰 세션을 통해 얻은 인사이트에 대한 내용들을 정리해보았습니다.

"코드는 읽는 것이 아니라 예측하는 것이다."

제3자가 코드를 봤을 때 "파악하기 쉽다"는 건 단순히 읽기 편한 게 아니라, 관련 파일을 일일이 따라가지 않아도 어떤 동작을 할지 예측 가능한 것이어야 한다는 뜻이었습니다.
처음 들었을 땐 생각해보지 못한 관점이었지만, 어떤 의도로 말씀하시는지 이해는 되었습니다. 깔끔함을 추구하다 보면 코드를 이리저리 나누고 옮기게 되는데, 그러다 오히려 전체 흐름을 파악하는 데 방해가 된 경험이 있었기 때문에 더욱 와닿았습니다.

인터페이스를 먼저 설계하기

라이브 코딩으로 직접 시연해 주셨는데, 기존 코드 분석부터 시작하는 게 아니라 핵심 요구사항에 대한 인터페이스를 새 파일을 생성해 제로베이스에서 먼저 설계한 뒤, 기존 코드를 그 인터페이스에 맞춰가는 방식으로 작업하셨습니다. 신선하게 다가왔던 점은 리팩토링이라 하면 '기존 코드 파악하고 이것을 기반으로 -> 책임과 역할 분리 or 추상화 or 도메인 분리' 흐름으로 작업하는걸 당연하게 생각해왔습니다.
인터페이스를 먼저 설계해두면 기존 코드와 대조했을 때 수정해야 할 부분이 훨씬 명확해지고, 작업 시작점도 더 선명해진다는 점에서 실용적인 접근법이라는 생각이 들었습니다.

setState를 이름 그대로 props로 전달할 때의 문제점

컨포넌트에 props를 전달할 때 setState라는 이름 그대로 넘기는 건 저도 자주 사용해오던 패턴이었습니다. setState 자체로 설명 가능한 의도를 담고 있다고 생각했거든요.

그런데 props를 받는 컴포넌트의 입장에서는 다릅니다. 만약 이 컴포넌트가 재사용성이 강한 컴포넌트라면 어떨까요?

예를 들어 DateSelector 컴포넌트는 날짜 값을 받아 계산하고 반환하면 그만입니다. 부모 컴포넌트에 어떤 상태가 있는지 알 필요가 없죠.
그런데 매개변수 이름이 setDate라면, "부모 컴포넌트에 date라는 state가 존재하고 그것을 set한다"는 외부 맥락을 컴포넌트 안에 끌어들이게 됩니다.

재사용성을 고려한다면 setDate 대신 onChange가 훨씬 자연스럽고 추상적으로 보입니다. DateSelector는 외부 맥락을 알 필요 없이 날짜를 반환하는 역할에만 집중할 수 있고, 순수성도 높아져 재사용 컴포넌트로서의 성격에 더 잘 맞습니다.

# DateSelector.tsx
interface DateSelectorProps {
  value?: string; // ✅ date 대신 value
  onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // ✅ setDate 대신 onChange
  ...
}
 
export const DateSelector = forwardRef<HTMLInputElement, DateSelectorProps>(
  ({ value, onChange, ... }, ref) => {
    const inputProps = value !== undefined ? { value } : { defaultValue };
    return (
      <input  onChange={onChange} ... />
    );
  }
);

후기

지금까지 "좋은 코드를 설계한다"는 건 저에게 꽤 막연하게 다가왔습니다.
고려해야 할 요소도 많고, 같은 코드를 보고도 A가 맞다고 하는 사람과 B가 더 낫다고 하는 사람이 공존한다고 생각하기 때문입니다. 세션에서도 언급됐듯, 좋은 코드에 정답은 없지만 내가 내린 선택을 명확한 근거로 설명할 수 있는 것이 중요하다는 걸 이번에 깨달았습니다.
그러려면 개발하면서 겪은 경험들, 문제에 직면했던 순간들을 그냥 흘려보내지 않고 사고하면서 내 것으로 만들어가는 태도가 필요하다고 느꼈습니다. 그게 결국 좋은 개발자로 성장하는 밑거름이 된다는 걸 다시금 자각한, 의미 있는 경험이었습니다.

댓글 영역을 준비하고 있습니다.