Hello, GSM
실제 신입생 모집에 쓰이는 입학지원 서비스에서 인증·원서 작성·관리자 출력과 공통 개발 기반을 개선했습니다

- 누적 사용자 1,800명 이상 (GA 기준)
- 외주 비용 약 1,000만 원 절감
- Warm Build 15초 → 1초
배경
Hello, GSM은 광주소프트웨어마이스터고등학교 신입생 모집에 실제로 사용하는 입학지원 서비스입니다. 지원자는 원서 작성과 합격 여부 확인을, 관리자는 원서 심사와 지원자 관리를 웹에서 처리합니다.
마이스터고등학교는 학교마다 입학 전형이 다릅니다. 그래서 이전에는 학교마다 외주업체에 맡겨 만든 입학지원 시스템으로 신입생 지원을 받고 관리했습니다. 이 방식은 외주 비용이 적지 않게 들었습니다. 선생님들은 프로그램을 어떻게 써야 할지 알기 어렵고 행정 처리가 번거롭다고 했고, 학생들도 사용하기 불편해 지원에 어려움을 겪었습니다. 해마다 바뀌는 입학 전형에 빠르게 대응하고, 누구나 쉽게 쓰고 운영할 수 있는 시스템이 필요했습니다. 그래서 교내 개발팀이 직접 개발하고 운영하게 됐습니다.
2026학년도 모집 기간에 GA 기준 누적 사용자 1,800명 이상이 이용했고, 자체 개발로 약 1,000만 원의 외주 비용을 절감했습니다.
맡은 일
2025년에는 프론트엔드 개발자로 서비스를 직접 개발하고 운영했습니다. 지원자용 client, 관리자용 admin, 두 앱이 공유하는 패키지를 함께 다뤘습니다. OAuth 콜백, 회원가입 인증, 원서 작성 중 이탈 방지처럼 실제 접수 흐름에 영향을 주는 문제를 해결했고, 모달 상태 관리와 운영 데이터 관리 방식을 정리했습니다.
2026년에는 서비스를 이어받은 후배들의 운영을 지원했습니다. 그 과정에서 Turborepo 기반 빌드 구조 재구성과 관리자 원서 출력 기능을 맡았습니다. 두 해에 걸쳐 PR 83개를 머지했습니다.
한 일
공통 패키지의 역할을 나누고 Turborepo 빌드 구조로 전환
- 문제: 공통 코드가
shared패키지에 집중되어 있었고, 반복 빌드가 오래 걸렸습니다. 앱 간 공통 설정과 비즈니스 로직을 별도 책임으로 나눌 필요가 있었습니다. 2026년 후배들의 운영을 지원하면서 진행한 작업입니다. - 해결: 기존 워크스페이스를
Turborepo기반으로 재구성했습니다.api,constants,types,utils,ui와 공통 설정 패키지를 분리하고 앱의 의존성을 연결했습니다.turbo.json에는 선행 패키지 빌드 의존성,.env*입력과 빌드 산출물 경로를 명시했습니다. 패키지 이동에 맞춰Dockerfile과 CI/CD 설정도 함께 수정했습니다. 첫 반영을 한 번 되돌린 뒤 보완해 다시 머지했습니다. #381 · #383 - 결과: 앱별 코드는 배포 단위로, 공유 코드는 역할별 패키지로 관리하도록 정리했습니다.
Cold Build는 23초 → 19초,Warm Build는 15초 → 1초로 줄었습니다.
모달의 호출부와 렌더링을 분리
- 문제: 각 컴포넌트가 모달의 로컬 상태와 UI를 개별 관리했습니다. 다른 화면에서 같은 모달을 재사용하려면 호출부마다 상태와 마크업을 다시 수정해야 했습니다.
- 해결:
Zustand의useModalStore에 열림 상태, 전달 데이터와 확인 콜백을 모았습니다.ModalContainer는AdminModals,ClientModals,SharedModals를 조합하도록 나눴습니다. 두 앱의 레이아웃에 컨테이너를 적용하고, 화면에서는 필요한 스토어 액션을 호출하도록 변경했습니다. #301 - 결과: 로그인 안내, 결과 발표, 서류 제출 상태 변경 등을 같은 호출 방식으로 관리하게 됐습니다. 화면의 업무 처리 코드와 모달의 UI·상태 정의를 분리해 재사용 지점을 공통 패키지로 모았습니다. #301
FAQ와 팀 소개를 코드 배포 없이 관리하도록 변경
- 문제: FAQ와 팀 멤버 목록을 정적
JSON으로 관리해 내용이 바뀔 때마다 PR과 배포가 필요했습니다. - 해결:
Notion Database API를 연결하고 FAQ·멤버 응답 타입을 정의했습니다. 서버에서 데이터를 읽고id순서로 정렬해 화면에 전달하도록 바꿨습니다. 기존JSON파일은 제거하고, 이스케이프된 따옴표 처리와 역할 명칭도 정리했습니다. #226 - 결과: 운영자가 Notion에서 내용을 바꾸면 코드 수정과 배포 없이 반영됩니다. 페이지는
revalidate: 3600으로 한 시간마다 데이터를 다시 읽습니다. #226
트러블슈팅
OAuth 실패를 프론트엔드에서 안내할 수 없었던 문제
- 상황: 로그인 도중 오류가 발생하면 백엔드의 Whitelabel 오류 페이지가 사용자에게 노출됐습니다.
- 원인: 로그인 직후 백엔드 OAuth 엔드포인트로 이동해 인증 코드 수신부터 토큰 교환까지 서버가 처리했습니다. 실패 응답도 백엔드 페이지로 표시되어 프론트엔드가 안내 UI를 제어할 수 없었습니다.
- 해결:
useOAuthLogin과 콜백 페이지를 만들었습니다. 프론트엔드는 제공자의 인증 화면으로 이동하고Authorization Code를 받아 백엔드 API로 전달하며, 백엔드는 토큰 교환과 사용자 정보를 처리한 뒤 JSON으로 결과를 반환하도록 역할을 나눴습니다. 이후state와 관리자 권한 확인에 따른 이동 경로를 정리하고, 콜백 중복 실행은useRef가드로 막았습니다. #222 · #233 · #367
원서 작성 보호가 정상적인 단계 이동까지 막았던 문제
- 상황: 원서 작성 중 페이지를 벗어나면 입력 내용이 사라질 수 있어 이탈 확인이 필요했습니다. 처음에는
router.push를 재정의해 내부 이동도 확인하도록 확장했습니다. #198 - 원인: 원서의 ‘다음으로’ 버튼도 같은
router.push를 사용해, 원서 작성 단계를 진행할 때마다 이탈 확인창이 표시됐습니다. 보호해야 할 사이트 이탈과 유지해야 할 단계 이동을 구분하지 못했습니다. - 해결: 라우터 재정의를 제거하고
beforeunload기반 경고를 유지했습니다./register에서는 헤더의 이동 링크를a태그로 바꿔 문서 이탈 경고가 적용되게 했습니다. 반면 원서 단계 이동은 기존 클라이언트 라우팅으로 유지했습니다. 리뷰를 반영해 헤더의a기반 내비게이션과Link기반 내비게이션도 별도 컴포넌트로 나눴습니다. 데이터 자동 저장이 아니라 실수로 떠나는 행동을 확인하는 방식입니다. #207