본문 바로가기
웹 프로그래밍/프론트엔드

[React] 자연어 기반 시간표 자동 생성을 위한 프론트엔드 최적화 여정

by me_in_sk 2026. 1. 10.
반응형

대학생 AI 챗봇 서비스 강냉봇에 "자연어 기반 시간표 자동 생성" 기능을 추가하면서, 기획 부재 상황에서 애자일하게 MVP를 구현하고, 프론트엔드 주도적인 개발 방식을 통해 난관을 극복한 과정을 공유합니다.

 

📑 바로가기


🎯 문제 정의: 수강신청은 왜 항상 힘들까?

대학생들에게 수강신청 기간은 전쟁입니다. 듣고 싶은 과목들은 많은데, 어떤 과목이 어떤 시간에 열리는지, 내 공강 날짜와 겹치지는 않는지 하나하나 따져봐야 합니다. 기존의 시간표 서비스들은 대부분 수동 검색 방식이었습니다.

"알고리즘 A반 넣어보고... 안되네? B반으로 바꿔볼까? 아 그럼 데이터베이스랑 겹치네..." 이런 반복적인 시행착오를 수십 번 겪어야 하나의 시간표가 완성됩니다.

핵심 질문: "알고리즘, DB, 컴네 듣고 싶은데 금요일 공강으로 짜줘" 처럼
자연어로 말하면 AI가 알아서 최적의 조합을 찾아줄 순 없을까?

이 단순한 질문에서 시작하여, 강냉봇의 핵심 기능인 AI 시간표 생성 시스템 프로젝트가 시작되었습니다.


🏗️ 시스템 아키텍처 및 설계

시간표 생성 시스템은 RAG (Retrieval-Augmented Generation) 기반의 백엔드와 고도화된 프론트엔드 상태 관리가 결합된 구조입니다.

시스템 흐름
시스템 흐름

 

컴포넌트 구조 설계

components/schedule/
├── ScheduleCanvas.tsx   # 메인 컨테이너 (상태 관리 및 레이아웃)
├── ScheduleGrid.tsx     # 시간표 시각화 (CSS Grid 기반)
├── ScheduleCarousel.tsx # 생성된 여러 시간표 슬라이더
├── FilterPanel.tsx      # 공강 요일/제외 시간대 필터
└── SavedScheduleList.tsx # 저장된 시간표 목록

ScheduleCanvas가 중앙 관제탑 역할을 하며, useScheduleStore를 통해 전체 애플리케이션의 시간표 데이터 흐름을 제어하는 구조로 설계했습니다.

강냉봇 웹(Web) vs 모바일(Mobile) 반응형 UI 예시
강냉봇 웹(Web) vs 모바일(Mobile) 반응형 UI 예시

 

🔥 기술적 도전과제

1. 기획의 부재와 촉박한 일정

가장 큰 난관은 역설적이게도 기술이 아닌 프로세스였습니다. 상세 기획이 없는 상태에서 백엔드 API가 나오길 기다리다가는 마감 기한을 맞출 수 없었습니다. 기존의 폭포수(Waterfall) 방식으로는 불가능했습니다.

2. 다양한 사용자 환경 (주간 vs 야간)

대학에는 주간 수업(09:00~18:00)만 듣는 학생도 있고, 야간 수업(~22:00)을 듣는 학생도 있습니다. 고정된 시간표 그리드를 사용하면 주간 학생에겐 불필요한 공백이 생기고, 야간 학생에겐 수업이 잘리는 문제가 발생합니다.

3. 실시간 필터링 성능 이슈

"금요일 공강으로 보여줘", "1교시는 빼줘" 같은 필터링을 할 때마다 매번 서버에 요청을 보내면, 네트워크 지연으로 인해 사용자 경험이 크게 저하됩니다.


💡 해결 과정

해결책 1: API-First & Agile 개발 전략

기획이 나올 때까지 기다리는 대신, 가설 단위로 MVP 기능을 테스트하며 프론트엔드가 주도적으로 API 명세를 정의하고 개발을 시작했습니다.

🚀 Frontend-First 접근법

  1. API 명세서 선행 작성: 프론트엔드 관점에서 필요한 데이터 구조를 정의하여 SCHEDULE_API_SPEC.md 작성
  2. Mock 서비스 구현: 실제 API 대신 더미 데이터를 반환하는 서비스 레이어 구현
  3. UI/UX 선 검증: 동작하는 MVP를 하루 만에 구현하여 팀원들에게 시연 및 피드백 수렴
  4. 계약 기반 통합: 백엔드 개발이 완료되었을 때, 미리 정의된 명세(계약) 덕분에 충돌 없이 통합

이 전략 덕분에 백엔드 개발자와 병렬로 작업할 수 있었고, 개발 생산성을 극대화할 수 있었습니다.


해결책 2: 동적 시간표 그리드 (Dynamic Grid)

사용자의 수강 신청 내역에 따라 시간표의 높이가 자동으로 조절되도록 구현했습니다.

주간 학생(좌) vs 야간 학생(우) 시간표 비교
주간 학생(좌) vs 야간 학생(우) 시간표 비교
동적 그리드 로직 다이어그램
동적 그리드 로직 다이어그램

 

💻 동적 범위 계산 로직

// ScheduleGrid.tsx - 실제 구현 코드
const DEFAULT_START_HOUR = 9;
const DEFAULT_END_HOUR = 19;  // 기본값 (주간 학생 기준)

const { startHour, endHour } = useMemo(() => {
  let minStartHour = DEFAULT_START_HOUR;
  let maxEndHour = DEFAULT_END_HOUR;

  courses.forEach((course) => {
    course.slots.forEach((slot) => {
      // 분 단위 → 시간 단위 변환 (올림)
      const slotEndHour = Math.ceil(timeToMinutes(slot.endTime) / 60);

      // 수업이 19시 이후까지 있으면 그리드 확장
      if (slotEndHour > maxEndHour) {
        maxEndHour = Math.min(slotEndHour, 24);  // 최대 24시
      }
    });
  });

  return { startHour: minStartHour, endHour: maxEndHour };
}, [courses]);

🎨 CSS Grid 활용

CSS Grid의 minmax()auto-fit을 활용하여, 시간대의 범위가 늘어나도 레이아웃이 깨지지 않고 각 교시(Time Slot)의 높이가 비율에 맞춰 자동으로 조절되도록 스타일링했습니다.


해결책 3: 클라이언트 사이드 필터링 (Client-Side Filtering)

필터링 시 발생하는 네트워크 비용을 0으로 만들었습니다. 이미 받아온 데이터(allSchedules)는 그대로 두고, 화면에 보여지는 데이터(generatedSchedules)만 로컬에서 가공합니다.

� 핵심 설계: Zustand 상태 관리

5개 컴포넌트가 동일한 시간표 데이터를 공유해야 했습니다. 단순히 useState로 관리하면 props drilling이 깊어지고, 필터링 시 원본 데이터를 잃어버릴 수 있었습니다.

  • Props Drilling 제거: 어떤 컴포넌트든 store에서 직접 데이터 접근
  • 원본/필터 분리: allSchedules(원본, 불변) ↔ generatedSchedules(필터 결과)
  • 일관성 유지: 저장/불러오기, 뷰 전환 등 모든 상태가 한 곳에서 관리
상태 관리 흐름
상태 관리 흐름

 

💻 실제 구현 코드

// useScheduleStore.ts - 실제 구현 코드

// 필터 적용 함수 (순수 함수)
const filterSchedules = (schedules: Schedule[], filters: ScheduleFilters) => {
  return schedules.filter((schedule) => {
    // 1. 공강 요일 체크 - 원하는 요일에 수업이 없어야 함
    const emptyDayPass = filters.emptyDays.every((day) =>
      schedule.emptyDays.includes(day)
    );
    if (!emptyDayPass) return false;

    // 2. 제외 시간대 체크 - 특정 시간에 수업이 없어야 함
    for (const course of schedule.courses) {
      for (const slot of course.slots) {
        for (const ex of filters.excludeTimeRanges) {
          if (slot.day === ex.day && timeRangesOverlap(...)) {
            return false;
          }
        }
      }
    }
    return true;
  });
};

// setFilters 액션 - 필터 변경 시 즉시 재계산
setFilters: (newFilters) => {
  set((state) => ({
    filters: { ...state.filters, ...newFilters },
    generatedSchedules: filterSchedules(state.allSchedules, updatedFilters),
  }));
}

이 결과, 사용자가 필터를 클릭하는 순간 0ms의 지연 시간으로 즉시 시간표가 변경되는 쾌적한 경험을 제공할 수 있었습니다.


보너스: 이미지 저장 최적화

단순히 화면을 캡처하는 것이 아니라, html-to-image를 활용하여 사용자 친화적인 저장 기능을 구현했습니다.

  • 고해상도 지원: pixelRatio: 2 설정으로 아이패드/맥북 등 Retina 디스플레이에서도 선명하게 저장
  • 다크모드 감지: 현재 테마를 감지하여 배경색을 자동으로 흰색(#fff) 또는 남색(#0f172a)으로 설정
  • 로딩 피드백: 이미지 생성 중 다운로드 버튼에 스피너를 표시하여 "멈춤"이 아닌 "작업 중"임을 인지시킴

📈 개선 결과

 항목  기존 방식  개선된 방식  효과
 조합 생성 시간  수작업 30분+  자연어 1문장  시간 단축 99%
 필터 반응 속도  API 호출 (수백ms)  즉시 (0ms)  UX 대폭 향상
 개발 생산성  Waterfall  Agile + API-First  2주 병렬 개발

배운 점

  1. 프론트엔드의 주도성: 기획이나 백엔드가 주어지길 기다리기보다, API 명세라는 "계약"을 먼저 제시함으로써 프로젝트 전체의 속도를 높일 수 있음을 배웠습니다.
  2. 사용자 중심의 최적화: 기술적으로 더 쉬운 방법(고정 그리드)보다 사용자에게 더 좋은 방법(동적 그리드)을 택했을 때의 만족도가 훨씬 큼을 확인했습니다.
  3. 클라이언트의 힘: 모든 것을 서버에 의존할 필요는 없습니다. 적절한 로컬 필터링은 서버 부하도 줄이고 사용자 경험도 개선하는 일석이조의 효과가 있습니다.

🛠 기술 스택 요약

 영역  기술  활용 내용
 상태 관리  Zustand  복잡한 시간표 데이터 및 필터 상태 관리
 이미지 처리  html-to-image  DOM 요소를 고해상도 PNG로 변환
 스타일링  Tailwind CSS  반응형 그리드 및 다크모드 구현
 다국어  i18next  4개 국어(한/영/중/일) 완벽 지원

시간표 짜는 스트레스에서 해방된 대학생들을 꿈꾸며.

📂 프로젝트 저장소: KangNaengBot-FE

🌐 배포 URL: KangNaengBot


◆ 함께 보면 좋은 글

반응형

댓글