Sleeep23' space
Front-end Engineer

같은 서비스를 4년동안 4번 만들면서 배운 것 - Gijol v1에서 v2.1까지

이 글은 같은 문제를 네 번 다시 만들며 내가 생각하는 “좋은 서비스”의 기준이 어떻게 바뀌었는지에 대한 기록이다.

2026-04-01 | 회고 | 20min


같은 문제를 네 번 마주했다: Gijol을 다시 만들며 바뀐 좋은 서비스의 기준

요약하면, Gijol은 GIST 학생들이 자신의 졸업요건 충족 여부를 확인하고 수강 계획을 세울 수 있도록 만든 서비스다. 2022년 v1 출시 이후 약 4년 동안 v1, v1.1, v2, v2.1를 거치며 실제 사용자 피드백, 기술 선택의 시행착오, 군 복무 중 운영 비용 문제, 그리고 AI를 활용한 재구현 경험을 거쳤다.

빠르게 보는 글의 구조

구간당시의 핵심 질문이 버전에서 배운 것
v1일단 학생들이 쓸 수 있는 서비스를 만들 수 있을까?실제 사용자를 만나야 문제와 피드백이 생긴다.
v1.1기술적으로 더 나은 구조로 바꾸면 더 좋은 서비스가 될까?코드 개선과 제품 가치 증가는 같은 문제가 아니다.
v2사용자가 진짜 서비스처럼 느끼게 만들 수 있을까?정보 전달 구조, 개인화, 인증, 운영 부담이 함께 따라온다.
v2.1혼자서도 지속 가능하게 운영할 수 있을까?덜어내는 선택도 서비스 개선이 될 수 있다.
이후다음 버전은 무엇을 해결해야 할까?도메인 규칙의 변화와 사용자 예외를 감당할 구조가 필요하다.

들어가며 — 같은 문제를 네 번 마주하기

문제를 다시 바라보게 된 이유

2022년 4월에 시작한 졸업요건 확인 서비스 Gijol을 2026년 2월까지 v1, v1.1, v2, v2.1로 네 번 다시 만들었다. 그 사이에는 군대 2년의 공백도 있었다.

한 프로젝트를 네 번 만든다는 건 같은 코드를 네 번 짠다는 뜻이 아니었다. 오히려 같은 사용자 문제를 네 번 다시 바라보는 일에 가까웠다. 네 번 모두 “더 잘 만들고 싶어서” 다시 만들었지만, 그때마다 내가 생각한 “더 잘”의 정의는 달랐다.

좋은 서비스에 대한 기준 변화

처음에는 일단 작동해서 학생들이 쓸 수 있는 것이 중요했다. 그다음에는 사용자가 진짜 서비스처럼 느낄 수 있는 완성도가 중요했다. 시간이 지나서는 운영 비용과 유지보수 부담을 감당할 수 있는 지속 가능성이 중요해졌다. 그리고 최근에는 AI를 활용해 기능 구현의 속도가 빨라진 만큼, 오히려 더 좋은 설계와 검증 기준이 중요하다는 생각을 하게 되었다.


v1 — 일단 써먹을 수 있게 만든다

당시의 문제의식

2022년 4월, 전기전자컴퓨터공학부를 선언한 직후였다. 이제 막 전공을 정했으니 앞으로 이 분야에서 어떻게 성장할 수 있을지 고민하던 시기였다. 어떤 수업을 들어야 하는지, 어떤 분야를 더 깊게 공부해야 하는지, 앞으로의 학업 루트를 어떻게 설계해야 하는지가 모두 막막했다.

문제는 정보를 얻을 수단이 많지 않았다는 점이었다. 당시에는 코로나 시기였고, 편하게 물어볼 수 있는 선배나 동기가 많지 않았다. 정보를 얻을 수 있는 창구는 커뮤니티와 학사편람 정도였다. 하지만 학사편람은 공식 문서이긴 해도, 학생 입장에서 “그래서 나는 어떤 과목을 들어야 하지?”라는 질문에 바로 답해주는 형태는 아니었다.

졸업요건 확인이 필요했던 이유

수업 선택의 중요한 기준 중 하나는 결국 졸업요건이었다. 어떤 전공을 선택하고, 어떤 과목을 듣고, 어떤 루트를 선택할지는 결국 “내가 졸업요건을 어떻게 충족할 것인가”와 연결되어 있었다. 그런데 학교에서 제공하는 별도의 졸업요건 확인 서비스는 없었고, 당시에는 학생이 수강한 과목을 제출하면 담당 부서에서 수기로 확인해주는 방식으로 알고 있었다.

물론 졸업요건 확인 로직이 단순하지 않기 때문에 그런 방식이 유지되었을 것이다. 입학연도, 전공, 학사편람 개정, 과목 인정 범위가 얽히면 사람이 확인하는 편이 더 안전하다고 판단했을 수도 있다. 하지만 학생 입장에서는 그 복잡함 때문에 오히려 더 큰 불편을 겪고 있었다. 졸업 직전에야 확인하는 것이 아니라, 학업 루트를 설계하는 과정에서 계속 확인할 수 있어야 하는 정보였기 때문이다.

그래서 학생들이 자신의 졸업요건 충족 상태를 직접 확인하고, 나아가 어떤 수업을 선택할지 조정하는 데 도움을 주는 서비스가 필요하다고 생각했다. 그러던 중 이미 같은 문제의식으로 진행되고 있던 프로젝트가 있었고, 그 프로젝트에 합류하면서 Gijol을 만들기 시작했다.

팀 구성과 초기 역할

처음 팀은 백엔드, 프론트엔드, 디자인 역할을 나누어 세 명으로 구성되었다. 당시 나는 개발 경험이 거의 없었기 때문에 처음부터 복잡한 비즈니스 로직을 구현하기보다는, 서비스가 어떤 문제를 어떤 흐름으로 해결해야 하는지 정리하고 프로토타입을 만드는 쪽에서 먼저 참여했다.

MVP를 작게 자른 방식

가장 먼저 집중한 것은 문제를 작게 자르는 일이었다. 졸업요건 확인은 학번, 전공, 이수 구분, 과목 인정 여부가 얽힌 복잡한 문제였지만, 첫 버전에서 모든 경우를 완벽히 다루려고 하면 아무것도 출시하지 못할 것 같았다. 그래서 MVP를 세 단계로 압축했다.

첫째, 개인 성적표를 업로드한다. 둘째, 전공을 선택한다. 셋째, 졸업요건 충족 결과를 확인한다.

결과 화면도 단순히 “충족/미충족”만 보여주는 방식으로 만들고 싶지는 않았다. 학생 입장에서 중요한 것은 현재 상태를 아는 것뿐 아니라, 그래서 다음에 무엇을 해야 하는지 판단하는 것이었기 때문이다. 그래서 결과를 영역별로 나누고, 각 영역마다 전반적인 달성 현황, 세부 이수 현황, 추천 사항을 보여주는 구조로 잡았다.

사용자 흐름과 프로토타입

초기 프로토타입은 Framer를 이용해 만들었다. 이 과정에서는 예쁜 화면을 만드는 것보다 사용자가 어떤 순서로 서비스를 이해하고 사용할지를 더 많이 고민했다. 졸업요건이라는 주제 자체가 학생에게 부담스러운 정보였기 때문에, 첫 화면에서 너무 많은 설명을 하기보다는 업로드, 전공 선택, 결과 확인이라는 흐름을 최대한 단순하게 유지하려 했다.

개발과 출시 과정

프로젝트 진행은 당시 많이 언급되던 애자일 방식에 가깝게 운영했다. 1주차에는 디자인과 사용자 흐름을 정리하고, 2~3주차에는 프론트엔드와 백엔드 개발을 진행하고, 4주차에는 최종 점검과 출시를 준비했다. 이후 한 달 정도는 실제 사용 중 발생한 오류를 수정하고 사용자 피드백을 수집하는 데 집중했다.

출시 후 반응

출시 후에는 에브리타임과 학번별·학과별 카카오톡 오픈채팅방에 서비스를 공유했다. 첫 주부터 예상보다 많은 사용자가 들어왔고, 특히 수강신청 기간에는 트래픽이 눈에 띄게 높아졌다. 이때 Gijol이 매일 접속하는 서비스라기보다는, 학생들이 수강 계획을 세우거나 졸업요건을 확인해야 하는 특정 시점에 강하게 필요해지는 서비스라는 것을 처음 체감했다.

가장 기억에 남는 반응은 주변 친구나 선배들이 직접 써보고 “이거 진짜 이쁘게 잘 만들었는데?”라고 말해줬을 때였다. 단순히 칭찬을 들어서 좋았다기보다는, 내가 프로토타입 단계에서 고민했던 사용자 흐름과 화면 구성이 실제 사용자에게 전달되었다는 느낌이 들었다. 개발 경험이 거의 없는 상태에서 시작했기 때문에, 내가 고민한 것이 사용자에게 닿았다는 성취감은 굉장히 컸다.

서비스가 어느 정도 알려진 뒤에는 더 신기한 경험도 있었다. 주변에서 나를 소개할 때 “Gijol 개발자”라고 말하면, “저 지졸 써봤어요. 엄청 좋았어요”라는 반응이 돌아오곤 했다. 그때 처음으로 이 프로젝트가 내 로컬 개발 환경 안에 있는 코드가 아니라, 학교 안에서 실제로 쓰이는 서비스가 되었다는 느낌을 받았다.

실제 사용자가 드러낸 예외 상황

물론 문제도 많았다. 가장 기억에 남는 오류는 해외 계절학기를 수강한 학생들의 성적표였다. 해당 과목명이 영어로 표시되면서 기존 과목명 기준의 로직으로는 제대로 인식하지 못했고, 이를 학교 과목과 호환되는 형태로 변환하는 처리가 필요했다. 또 성적표 엑셀 파일의 형식이 예상과 다르거나 영어로 되어 있는 경우, 파싱 과정에서 서비스가 무한 로딩에 빠지거나 비정상적으로 처리되는 문제도 있었다.

이때 에러 처리와 테스트 코드의 부재를 처음으로 크게 체감했다. 개발할 때는 정상적인 성적표 파일 하나가 잘 동작하는지만 확인했지만, 실제 사용자는 훨씬 다양한 형태의 데이터를 가져왔다. 배포 이후에야 “내가 생각한 입력”과 “사용자가 실제로 넣는 입력”이 다르다는 사실을 배웠다.

v1에서 배운 것

v1의 가장 큰 성과는 완벽한 졸업요건 계산기가 아니었다. 작동하는 서비스를 학교 안에 내놓았고, 실제 학생들이 수강신청과 졸업요건 확인 시점에 반복적으로 사용하게 만들었다는 점이었다. 사용자가 없으면 오류도 발견할 수 없고, 피드백도 받을 수 없으며, 다음 버전의 이유도 생기지 않는다. 그런 의미에서 v1은 부족했지만 반드시 필요했던 버전이었다.


v1.1 — 출시되지 못한 버전

다시 만들고 싶었던 이유

v1을 출시한 뒤 가장 먼저 눈에 들어온 것은 코드와 개발 환경이었다. 당시에는 CRA 기반으로 React 프로젝트를 만들었는데, Vite가 더 빠르고 번들 사이즈도 작다는 이야기를 자주 접했다. “CRA보다 좋다는데, 한 번 써보면 사용자 경험도 좋아지지 않을까?”라는 생각이 들었다.

기능도 아직 많지 않았기 때문에, 기존 코드를 크게 잃을 부담도 적었다. 그래서 단순히 빌드 도구만 바꾸는 것이 아니라, 리팩토링을 겸해 서비스를 다시 작성해보기로 했다. 기존에는 화면 흐름을 따라 상태가 내려가는 waterfall 형태에 가까웠는데, v1.1에서는 Context API를 활용해 전역적으로 필요한 상태를 더 명확하게 공유해보고 싶었다. 또 기능이 너무 적다는 생각도 있었기 때문에, 검색 관련 기능을 추가해보려는 계획도 있었다.

돌이켜보면 이때의 동기는 절반은 기술적 호기심이었고, 절반은 “이미 한 번 출시한 서비스를 조금 더 제대로 된 구조로 다시 만들어보고 싶다”는 마음이었다.

CRA에서 Vite로 전환한 실험

v1과 v1.1을 비교하기 위해 Docker로 동일한 Node 16 환경을 구성하고 빌드 결과를 비교했다. CRA 기반 v1의 production build output은 약 5.4MB였고, Vite 기반 v1.1의 dist output은 약 1.1MB였다. 빌드 시간도 2.1초 정도로 확실히 빨랐다.

처음에는 이 결과만 보고 Vite 전환이 명확히 더 낫다고 생각했다. 하지만 자세히 보면 주요 JS chunk가 gzip 기준 약 309KB로 컸고, 단일 chunk warning도 발생했다. 이때 처음으로 “전체 output 크기가 줄었다”는 사실과 “사용자가 실제로 처음 불러오는 코드가 충분히 작다”는 사실은 다를 수 있다는 걸 알게 됐다.

그래도 Gijol의 사용 패턴을 생각하면 Vite 전환은 충분히 의미가 있다고 보았다. Gijol은 사용자가 페이지 안에서 매우 빈번한 인터랙션을 반복하는 서비스라기보다는, 성적표를 업로드하고 졸업요건 검증 결과를 확인하는 흐름이 핵심이었다. 따라서 복잡한 런타임 인터랙션보다 초기 진입의 가벼움과 배포의 단순함이 더 중요했다. 그런 점에서 경량화된 빌드 결과와 빠른 개발 경험은 운영하는 입장에서 분명한 장점이 있었다.

빌드 도구 전환만으로는 부족했다

다만 이 실험을 통해 배운 것은, 빌드 도구를 바꾸는 것만으로 사용자-facing 성능이 자동으로 개선되지는 않는다는 점이었다. 번들러를 바꾸는 것과 앱 구조를 최적화하는 것은 다른 문제였다. code splitting, chunk 관리, 실제 사용자 흐름 기준의 로딩 전략 같은 고민이 함께 있어야 했다.

출시되지 못한 이유

기술적으로는 의미 있는 실험이었지만, v1.1은 끝까지 출시되지 못했다. 가장 큰 이유는 출시 동력이 약했다는 점이다. v1.1은 사용자에게 완전히 새로운 가치를 주는 버전이라기보다는, 내가 기존 코드를 더 좋은 구조로 다시 정리해보는 버전에 가까웠다. 검색 기능 같은 추가 아이디어는 있었지만, “이 버전이 꼭 나가야 한다”는 명확한 제품적 이유는 부족했다.

개인적인 동력의 소진

게다가 당시에는 실질적으로 혼자 개발을 이어가는 상황이었다. 처음에는 Vite, Context API, 리팩토링 같은 기술적 흥미가 있었지만, 시간이 지나면서 그 흥미만으로 서비스를 끝까지 밀고 가기는 어려웠다. 자연스럽게 학업 비중이 높아졌고, Gijol에 대한 집중도도 떨어졌다.

여기에 같은 시기 진행하던 다른 프로젝트에서의 협업 실패가 겹쳤다. Gijol 자체의 협업 문제는 아니었지만, 그 경험은 개발에 대한 감정 자체를 크게 흔들었다. 정말 내가 개발에 관심이 있는 건지, 제대로 배우고 있는 건지 의심하게 됐다. 가장 개발에 관심이 많았던 시기에 제대로 된 프로젝트 하나 완성하지 못하고 스트레스만 받았다는 자책감도 컸다. 어느 순간에는 모니터와 노트북을 보는 것조차 싫을 정도로 개발이 멀게 느껴졌다.

좋아하는 일인데, 막상 하려고 하면 그때의 기억이 같이 떠올라서 피하게 되는 상태에 가까웠다. 그래서 v1.1은 명확한 종료 선언 없이 자연스럽게 멈췄다. 아쉬움은 있었지만, 그 시점의 나는 그 버전을 끝까지 밀고 갈 만큼의 에너지를 갖고 있지 못했다.

시간이 지나 그 기억이 조금씩 사그라든 뒤에야 다시 개발을 하고 싶다는 마음이 돌아왔다. 그리고 이전에 끝내지 못했던 서비스 재출시를 다시 해보고 싶어졌다. 그렇게 v1.1에서 멈췄던 구조 개선의 경험은 v2의 출발점이 되었다.

v1.1에서 배운 것

v1.1을 통해 배운 것은 두 가지였다. 첫째, 기술적으로 더 나은 선택처럼 보이는 것도 실제 사용자 경험으로 이어지려면 별도의 검증이 필요하다는 점이다. CRA에서 Vite로 바꾸면 개발 경험과 빌드 결과는 좋아질 수 있지만, 그것만으로 사용자가 체감하는 서비스 가치가 커지는 것은 아니었다.

둘째, 내부 구조 개선만으로는 출시 동력을 만들기 어렵다는 점이다. 사용자가 왜 이 버전을 다시 써야 하는지 설명할 수 없다면, 만드는 사람도 끝까지 밀고 가기 어렵다. v1.1은 실패한 버전이라기보다는, 내가 제품의 변화와 코드의 변화를 구분하기 시작한 버전이었다.


v2 — 사용자가 진짜 서비스라고 느끼게 만든다

다시 시작한 이유

v2를 다시 만들기로 한 가장 큰 이유는 아쉬움이었다. v1.1을 출시까지 끌고 가지 못한 아쉬움도 있었고, 이미 배포되어 쓰이고 있던 v1을 다시 봤을 때 느껴지는 아쉬움도 컸다. 분명 내가 만든 서비스였지만, 시간이 지나 다시 보니 완성도가 부족했다.

특히 UI가 아쉬웠다. v1은 성적표를 업로드하고, 전공을 선택하고, 결과를 확인하는 최소 흐름에 집중한 버전이었다. MVP로서는 맞는 판단이었지만, 이후 기능을 확장하기에는 구조가 좁아 보였다. 졸업요건 확인을 넘어 학업 계획, 전공 선택, 부전공·복수전공 관리까지 다루려면 단순한 입력-결과 페이지가 아니라, 사용자의 학업 상태를 계속 보여줄 수 있는 대시보드 형태가 필요하다고 생각했다.

무엇보다 문제 자체는 여전히 남아 있었다. v1을 출시한 뒤에도 학생들이 졸업요건을 확인하는 과정은 여전히 불편했고, 부전공·복수전공, 교환학생 학점, 재수강, W드롭 같은 케이스는 계속 문의로 들어왔다. 당시에는 우리가 제시한 해결책을 뛰어넘는 뚜렷한 대안도 없다고 느꼈다. 불편함은 그대로인데, 그 문제를 서비스 형태로 풀고 있는 곳은 사실상 Gijol밖에 없었다.

그래서 v2부터는 단순히 “졸업요건을 계산해주는 도구”를 넘어, 더 개인화된 학업 관리 서비스로 확장해야겠다는 책임감이 생겼다. 나아가 이 구조를 잘 일반화하면 GIST뿐 아니라 다른 학교에도 적용할 수 있지 않을까, 그러면 수익화 가능성도 있지 않을까 하는 막연한 기대도 있었다. 지금 돌아보면 꽤 큰 상상이었지만, 그 기대가 다시 서비스를 만들게 하는 동력이 되었다.

정보 전달 중심의 대시보드로 전환

v2에서 가장 크게 바꾸고 싶었던 것은 사용자가 서비스를 받아들이는 방식이었다. v1은 성적표를 업로드하고 결과를 확인하는 흐름 자체는 단순했지만, 결과 화면은 지금 돌아보면 정보성 서비스라기보다 예쁜 인터랙션에 더 가까웠다. 영역별 정보를 확인하려면 사용자가 각각의 UI를 눌러 들어가야 했고, 전반적인 현황이나 세부 목록이 한눈에 들어오는 구조는 아니었다.

졸업요건 확인 서비스의 본질은 결국 정보 전달이라고 생각했다. 사용자는 예쁜 카드를 하나씩 열어보고 싶은 것이 아니라, 지금 내가 어느 영역을 충족했고, 어떤 영역이 부족하며, 앞으로 무엇을 들어야 하는지를 빠르게 알고 싶어 한다. 그래서 v2에서는 결과를 숨겨두는 방식보다, 중요한 데이터를 처음부터 바로 확인할 수 있는 대시보드 구조로 바꾸려고 했다.

이를 위해 메인 화면과 결과 화면을 전반적으로 다시 설계했다. 홈 화면에서는 현재 이용 가능한 기능과 개발 중인 기능을 구분해 보여주고, 대시보드 안에서는 사이드바를 통해 내 강의 현황, 졸업요건, 강의 정보 확인 기능으로 이동할 수 있게 했다. 학기별 강의 이수 현황은 막대그래프로 보여주고, 총 이수 학점, 최저 이수 영역, 피드백 개수 같은 요약 정보는 카드 형태로 배치했다. 졸업요건 현황은 영역별 충족도를 progress bar와 표로 정리해, 사용자가 클릭을 반복하지 않아도 현재 상태를 바로 파악할 수 있게 했다.

개인화 서비스로 확장하려던 시도

로그인을 도입한 것도 같은 맥락이었다. 단순히 계정을 만들고 싶었던 것이 아니라, 동의한 사용자들의 수강 루트를 학번별로 공유하고, 다른 사람들의 수강 흐름을 참고해 자신의 수강 계획을 세울 수 있는 서비스를 만들고 싶었다. 졸업요건 확인에서 끝나는 것이 아니라, “나와 비슷한 전공·학번의 학생들은 어떤 과목을 어떤 순서로 들었는가”를 볼 수 있다면 학업 계획 자체를 돕는 서비스가 될 수 있다고 생각했다.

기술 스택 선택

v2에서는 Next.js, Mantine, Recoil, NextAuth를 중심으로 다시 구현했다. Next.js를 선택한 이유는 솔직히 말하면 SSR이 반드시 필요해서라기보다는, 기술적 관심과 라우팅 편의성, 그리고 SEO에 대한 기대가 섞여 있었다. 당시에는 Next.js를 쓰면 서비스가 더 “제대로 된 웹 서비스”에 가까워질 것 같다는 기대가 있었다.

Mantine을 도입한 이유는 더 명확했다. 1인 프론트엔드 개발 환경에서 디자인 일관성을 빠르게 확보하고 싶었다. Chakra UI와 MUI도 후보였지만, Mantine의 기본 톤과 레이아웃 컴포넌트가 내가 만들고 싶은 서비스 이미지에 가장 가까웠다. 실제로 Mantine은 v2에서 가장 만족도가 높았던 선택 중 하나였다. 컴포넌트 작성과 스타일링에 쓰는 시간을 크게 줄여줬고, 대시보드 형태의 화면을 빠르게 구성할 수 있게 해줬다.

데이터로 확인한 사용 패턴

v2부터는 사용자 분석도 더 적극적으로 보기 시작했다. v1에서는 Google Analytics를, v2에서는 Vercel Analytics를 활용했다. 데이터를 보면서 Gijol이 매일 접속하는 서비스라기보다는 수강신청 시즌과 졸업요건 확인 시점에 집중적으로 사용되는 서비스라는 점을 더 분명히 알게 됐다. v1에서 최대 접속자 수가 50명대였던 것에 비해 v2에서는 280명대까지 올라갔고, 이는 UI와 서비스 구조 개선이 실제 사용으로 이어졌다는 신호처럼 보였다. 동시에 리텐션이 충분히 높지 않다는 점도 확인했다. 한 번 졸업요건을 확인하고 나가버리는 서비스를 넘어서려면, 사용자가 다시 들어올 이유가 되는 기능이 필요했다.

Next.js와 상태 복원의 시행착오

하지만 Next.js를 쓰면서 예상하지 못한 문제도 겪었다. 대표적인 것이 Recoil 상태와 sessionStorage를 함께 사용하면서 발생한 hydration mismatch였다. 중간에 Recoil 값을 sessionStorage에서 읽어와 바꾸는 로직을 넣어두었는데, SSR 시점에는 sessionStorage에 접근할 수 없으므로 상태가 기본값으로 렌더링되었다. 이후 브라우저에서 클라이언트 렌더링이 진행되면서 sessionStorage 값을 읽고 Recoil 상태가 바뀌었고, 이 때문에 서버에서 만들어진 HTML과 클라이언트에서 만들어진 결과가 달라졌다.

React의 hydration은 기본적으로 서버에서 만들어진 결과와 클라이언트 렌더 결과가 동일하다는 가정 위에서 동작한다. 그런데 클라이언트에서만 실행되는 상태 복원 로직이 렌더 결과를 바꿔버리면서 hydration mismatch가 발생했다. 당시에는 useEffect로 hydration 여부를 확인하는 훅을 두고, 클라이언트 마운트 이후에만 sessionStorage에 의존하는 값을 보여주는 방식으로 우회했다.

이 방법으로 문제는 해결했지만, 지금 돌아보면 완전한 설계 해결이라기보다는 당시의 이해 수준에서 안정적으로 동작하게 만든 우회에 가까웠다. 정확히 어떤 지점에서 mismatch가 발생했고, 상태 복원 전략을 어떻게 설계했어야 더 자연스러웠는지까지 완전히 이해했다고 보기는 어려웠다. 다만 이 경험을 통해 Next.js가 단순히 라우팅이 편한 React가 아니라, SSR과 CSR의 경계를 계속 의식해야 하는 프레임워크라는 것을 배웠다.

인증 구현에서 겪은 문제

로그인 구현에서도 비슷한 시행착오가 있었다. 처음에는 NextAuth를 이용해 로그인 기능과 refresh token rotation을 구현하려 했다. 그런데 update 함수를 useEffect의 의존성에 넣고, 그 effect 안에서 다시 update를 호출하는 구조를 만들면서 무한 토큰 갱신 문제가 발생했다. 세션이 갱신되면 useSession에 의해 리렌더링이 일어나고, 다시 effect가 실행되면서 update가 반복되는 식이었다.

이후에는 세션 만료 시점에 API route의 NextAuth callback을 통해 refresh가 일어나도록 구조를 바꾸려 했지만, 인증과 인가의 책임을 프론트엔드와 백엔드 사이에서 어떻게 나눌지 다루기가 점점 어려워졌다. 로그인 자체에 쓰는 시간이 길어지면서, 정작 서비스 핵심 기능 개발 속도도 느려졌다.

Clerk 도입과 남은 기술 부채

그래서 결국 Clerk을 도입했다. 당시 Clerk은 MAU 5,000명까지 무료였고, 프론트엔드와 백엔드 각각에 SDK를 제공했다. 사용자 정보를 관리할 수 있는 어드민 환경과 webhook 기능도 제공했기 때문에, 빠른 출시가 우선이었던 v2 상황에서는 매우 적절한 선택이었다. Clerk을 쓰면서 로그인과 사용자 관리에 쓰는 시간을 크게 줄이고, 다시 서비스 기능 개발에 집중할 수 있었다.

다만 이 선택은 아쉬움도 남겼다. 빠르게 출시하는 데는 성공했지만, 인증과 인가를 직접 깊게 다뤄보지 못했다는 기술 부채 의식이 남았다. v2에서 나는 외부 도구가 개발 속도를 얼마나 크게 높여줄 수 있는지 배웠지만, 동시에 그 도구가 대신 처리해준 영역을 내가 충분히 이해하지 못하면 언젠가 다시 학습해야 할 부채로 남는다는 것도 배웠다.

v2 출시 이후의 반응

v2를 출시한 뒤에는 학교 안에서 Gijol의 인지도가 더 커졌다는 것을 체감했다. 내가 Gijol을 개발한 사람이라고 말하면 주변에서 이미 알고 있는 경우가 많았고, “훨씬 예뻐졌다”, “이전보다 쓰기 좋아졌다”는 피드백도 자주 들었다. v1에서 실제 사용자를 얻었다면, v2에서는 사용자가 Gijol을 조금 더 완성된 서비스처럼 받아들이기 시작했다고 느꼈다.

리텐션 문제와 확장 방향

하지만 동시에 한계도 분명했다. 수강신청 시즌이 지나면 일 사용자 수는 4~5명 수준으로 떨어졌다. 대부분의 사용자는 필요할 때 들어와 졸업요건을 확인하고, 그 목적을 달성하면 다시 떠났다. 이 데이터를 보면서 Gijol은 강한 필요를 가진 서비스이지만, 사용 시점이 매우 제한적인 서비스라는 것을 알게 되었다.

그래서 리텐션을 만들려면 졸업요건 확인만으로는 부족하다고 생각했다. 사용자가 더 자주 들어올 이유를 만들려면, 학교생활 전반에서 필요한 정보를 다루어야 했다. 당시에는 재학 학기와 학점, 졸업요건 충족 여부, 시간표 같은 학업 상태 정보부터 강의 정보, 강의 평가, 강의 공지, 건물 위치, 기숙사 정보, 학내 공지, 동아리와 스터디 홍보까지 다양한 기능을 떠올렸다.

물론 모든 것을 만들 수는 없었다. 이미 학교의 기존 시스템이 제공하고 있는 경험은 우선순위를 낮추고, 학생이 실제로 불편해하지만 아직 잘 제공되지 않는 영역부터 구현해야 한다고 생각했다. 그 기준에서 시간표, 강의 정보, 졸업요건, 수강 루트 추천 같은 기능이 다음 확장 후보가 되었다.

지속 가능한 운영에 대한 고민

다만 현실적으로 인력 부족이 가장 큰 한계였다. v2에서도 일정 기간 협업은 있었지만, 대부분의 유지보수와 개선은 혼자 감당해야 했다. 서비스가 커질수록 만들고 싶은 기능은 늘어났지만, 그만큼 운영과 유지보수 부담도 함께 커졌다. 이때부터 Gijol을 더 크게 만드는 것만큼이나, 내가 지속적으로 운영할 수 있는 형태로 만드는 것이 중요하다는 생각이 생기기 시작했다.


v2에서 v2.1로 — 군대 2년의 공백

군 복무 중에도 계속된 서비스 운영

군 복무 중에도 Gijol을 사용하는 학생들이 완전히 사라진 것은 아니었다. 수강신청이나 졸업요건 확인이 필요한 시기에는 여전히 서비스에 들어오는 사용자가 있었다. 하지만 직접 유지보수를 하기 어려운 상황에서 서비스가 계속 운영된다는 것은 생각보다 부담이 컸다.

비용과 유지보수 부담

가장 현실적으로 느껴진 것은 비용이었다. 서버비와 Vercel Team Plan 등을 포함해 매달 약 5만 원 정도의 비용이 계속 나갔다. 큰 회사나 팀 입장에서는 작은 금액일 수 있지만, 군 복무 중인 개인 개발자에게는 적지 않은 부담이었다. 특히 Gijol이 매일 높은 트래픽을 받는 서비스가 아니라, 수강신청이나 졸업요건 확인 시점에 집중적으로 쓰이는 서비스라는 점을 생각하면 상시 서버 운영이 정말 필요한지 의문이 들기 시작했다.

유지보수의 어려움도 있었다. 군 복무 중 에러 문의가 들어온 적이 있었고, 나는 팀원에게 문제 상황을 공유하고 설명하는 방식으로 대응했다. 기억나는 것은 서버에서 값이 제대로 불러와지지 않는 오류였고, DevTools와 서버 로그를 확인하며 프론트엔드보다는 서버 쪽 문제라고 판단했던 장면이다. 정확한 원인과 해결 과정은 백엔드를 담당했던 팀원이 처리했고, 당시 기록을 충분히 남겨두지 않아 지금은 자세히 복기하기 어렵다. 다만 군 복무 중에는 이런 문제를 내가 즉시 들여다보고 고치기 어렵다는 사실 자체가 큰 부담이었다.

Next.js 13 계열의 보안 이슈를 확인하고 급히 버전을 올렸던 기억도 있다. 서비스는 돌아가고 있었지만, 내가 계속 가까이 붙어서 관리할 수 있는 상태는 아니었다. 이때부터 Gijol을 더 크게 만드는 것보다, 내가 지속적으로 감당할 수 있는 형태로 바꾸는 것이 먼저라는 생각이 강해졌다.

질문의 변화: 기능 확장보다 운영 가능성

그래서 자연스럽게 질문이 바뀌었다. “어떤 기능을 더 붙일까?”가 아니라, “이 서비스에 서버와 로그인이 정말 필요한가?”를 묻게 되었다. v2에서는 개인화와 수강 루트 공유를 위해 로그인과 서버를 도입했지만, 실제 핵심 경험은 여전히 성적표를 업로드하고 졸업요건을 확인하는 것이었다. 이 경험만 놓고 보면 사용자가 반드시 로그인해야 할 이유는 크지 않았다.

오히려 로그인과 서버를 덜어내고, 엑셀 파싱과 졸업요건 검증 로직을 프론트엔드로 옮기는 편이 더 적합할 수 있다고 생각했다. 물론 그만큼 클라이언트의 부담은 늘어난다. 하지만 Gijol은 대규모 실시간 상호작용이 필요한 서비스가 아니라, 사용자가 파일을 업로드하고 규칙 검증 결과를 확인하는 서비스에 가까웠다. 그렇다면 약간의 클라이언트 부하를 감수하더라도, 운영 비용을 거의 0에 가깝게 줄이고 진입 장벽을 낮추는 쪽의 이득이 더 크다고 판단했다.

전역 후 다시 확인한 수요

전역 후에는 수강신청까지 남은 시간이 두 달도 채 되지 않았다. 다시 만들기 전에 에브리타임에 글을 올려 실제로 이런 서비스가 필요한지 확인했고, 원하는 학생들이 여전히 많다는 것을 알게 되었다. 그 반응을 보고 이번 수강신청 기간 전에는 반드시 다시 출시해야겠다고 마음먹었다.

짧은 기간 안에 다시 출시하기

실제 개발은 1월 중반부터 2~3주 정도 집중해서 진행했다. 빠른 출시가 필요했지만, v1처럼 기능이 부족한 상태로 내고 싶지는 않았다. 그래서 졸업요건 확인뿐 아니라 시간표, 강의 검색, 로드맵 같은 기능을 함께 넣어 출시했다. 결국 v2.1는 더 많은 것을 쌓아 올린 버전이라기보다, 서버와 로그인을 덜어내면서도 학생들이 바로 사용할 수 있는 핵심 기능은 더 넓힌 버전이었다.


v2.1 — 덜어내고 지속 가능하게 만든다

서버와 로그인을 덜어낸 구조

v2.1에서 가장 먼저 바꾼 것은 운영 구조였다. 로그인과 서버를 제거하고, 서버리스에 가까운 형태로 Vercel에 배포했다. 핵심 로직을 프론트엔드로 옮기면서 운영 비용을 거의 0에 가깝게 줄이고, 사용자가 로그인 없이 바로 서비스를 사용할 수 있게 했다.

shadcn/Tailwind로의 전환

기술 스택에서도 변화가 있었다. v2에서 사용했던 Mantine 대신 shadcn/Tailwind 기반으로 전환했다. Mantine은 v2에서 빠르게 완성도 있는 UI를 만드는 데 큰 도움이 되었지만, v2.1에서는 컴포넌트 코드를 프로젝트 내부에서 직접 소유하고 수정할 수 있는 구조가 더 적합하다고 느꼈다. 특히 AI와 함께 코드를 분석하고 수정하는 과정에서는, 외부 라이브러리의 추상화된 컴포넌트보다 프로젝트 안에 존재하는 컴포넌트를 직접 다루는 방식이 더 편했다.

기능 확장의 기준: 졸업요건 이후의 행동

v2.1에서 시간표, 강의 검색, 로드맵을 넣은 이유는 각각 따로 떨어진 기능을 추가하기 위해서가 아니었다. 졸업요건 확인 이후의 행동까지 같은 서비스 안에서 이어지게 만들고 싶었다.

v2까지의 Gijol이 “내가 졸업요건을 얼마나 채웠는가”를 보여주는 서비스였다면, v2.1에서는 “그 결과를 바탕으로 앞으로 어떤 수업을 들을 것인가”까지 이어지는 흐름을 만들고 싶었다.

시간표: 다음 학기 계획으로 이어가기

시간표는 그 흐름의 첫 번째 확장이었다. 사용자가 졸업요건 결과를 보고 부족한 영역을 확인했다면, 그다음 행동은 자연스럽게 “그럼 다음 학기에 어떤 강의를 들어야 하지?”로 이어진다. 이 흐름을 서비스 안에서 끊기지 않게 만들고 싶었다. 당장은 사용자가 직접 강의를 배치하는 수준이지만, 장기적으로는 졸업요건 기반 강의 추천과 연결하고 싶었다. 사용자의 이수 현황, 남은 졸업요건, 개설 강의 정보를 바탕으로 다음 학기 후보 강의를 추천하고, 이를 시간표 위에서 바로 조정할 수 있는 구조를 생각했다.

강의 검색: 부족한 영역에서 강의 탐색으로

강의 검색도 같은 이유로 추가했다. 졸업요건 결과에서 특정 영역이 부족하다고 알려주는 것만으로는 충분하지 않았다. 사용자는 곧바로 “그 영역을 채울 수 있는 강의가 무엇인지”, “그 강의가 언제 열리는지”, “내 시간표에 넣을 수 있는지”를 알고 싶어 한다. 기존에는 졸업요건 확인과 강의 정보 탐색이 서로 다른 시스템과 문서에 흩어져 있었다. v2.1에서는 이 간극을 줄이고 싶었다.

로드맵: 저학번의 학업 루트 설계

로드맵은 특히 저학번과 전공 선택을 고민하는 학생을 위한 기능이었다. 졸업요건은 “무엇을 반드시 채워야 하는가”를 알려주지만, 학생이 실제로 궁금해하는 것은 종종 그보다 앞선 질문이다. “이 전공을 선택하면 보통 어떤 순서로 수업을 듣는가”, “어떤 기초 과목을 먼저 들어야 이후 과목을 이해할 수 있는가”, “이 분야로 가려면 어떤 루트가 자연스러운가” 같은 질문이다.

과거에는 학과별 로드맵 자료가 유용하게 활용되었지만, 현재 학사편람에서는 그런 정보가 충분히 드러나지 않는다고 느꼈다. 그래서 v2.1에서는 로드맵 기능을 추가해, 단순히 졸업요건 충족 여부를 넘어서 사용자가 자신의 학업 루트를 설계하는 데 필요한 정보를 제공하고 싶었다.

졸업요건 검증 로직의 재구성

졸업요건 확인 기능은 v2에서 백엔드가 담당하던 로직을 프론트엔드로 옮기면서 처음부터 다시 구성했다. 서버와 로그인을 덜어낸 만큼, 엑셀 파싱과 검증 로직을 클라이언트에서 처리해야 했다. 구조는 기초 이수 영역부터 전공 이수 영역까지, 영역별 요구사항을 순차적으로 판별하는 방식으로 설계했다.

아직 남은 UX 과제

다만 이 부분에는 여전히 아쉬움이 남는다. 졸업요건 검증은 단순히 과목 목록을 영역별로 합산하는 문제가 아니다. 같은 과목이라도 사용자가 어떤 분류로 인정받고 싶은지에 따라 결과가 달라질 수 있고, 전공·부전공·복수전공이 얽히면 한 과목을 어느 영역에 배치하는지가 중요해진다. 이 과정을 사용자가 자연스럽게 조정할 수 있어야 했지만, v2.1에서는 아직 충분히 매끄럽게 구현하지 못했다.

현재는 사용자가 업로드한 엑셀 정보를 수정하고, 다시 연산 버튼을 눌러 결과를 확인하는 방식에 가깝다. 동작은 하지만, 사용자의 의도를 자연스럽게 반영하는 UX라고 보기는 어렵다. 다음 단계에서는 사용자가 과목별 인정 영역을 직접 조정하고, 그 선택이 졸업요건 결과에 즉시 반영되는 구조가 필요하다고 느낀다.

서비스의 첫인상 개선

v2.1에서는 기능뿐 아니라 서비스가 사용자에게 처음 보이는 방식도 더 신경 썼다. v2에서 대시보드 형태의 UI를 만들었다면, v2.1에서는 자잘한 애니메이션, 폰트, OG 이미지, meta 데이터 같은 부분까지 챙기려 했다. 사용자가 링크를 처음 공유받았을 때 보이는 이미지와 설명, 페이지에 들어왔을 때의 움직임, 전체적인 타이포그래피도 서비스의 신뢰감에 영향을 준다고 생각했기 때문이다.


AI를 어디에 어떻게 썼는가

AI를 코드 생성기가 아니라 검증 가능한 보조 도구로 쓰기

v2.1는 1인 개발에 가까운 상황에서 짧은 기간 안에 다시 출시해야 했기 때문에 AI 에이전트를 적극적으로 활용했다. 다만 AI를 단순히 “코드를 대신 작성하는 도구”로 쓰기보다는, 큰 변경을 작은 단위로 나누고, 테스트와 검수 기준 안에서 구현을 반복하는 보조 도구로 사용하려 했다.

테스트와 더미 데이터로 만든 울타리

가장 중요했던 부분은 졸업요건 검증 로직이었다. 졸업요건 검증은 사용자의 실제 수강 계획에 영향을 줄 수 있기 때문에, AI가 작성한 코드를 그대로 신뢰할 수는 없었다. 그래서 먼저 더미 데이터를 만들고, 해당 데이터에 대해 어떤 결과가 나와야 하는지를 명확히 정의했다. 그다음 AI에게 이 기대 결과를 기준으로 테스트 코드를 작성하게 했다.

구현은 그 테스트를 통과하는 것을 기준으로 진행했다. 처음부터 모든 예외를 완벽하게 처리하려 하기보다는, 기본적인 검증 흐름을 만들고, 이후 여러 예외 상황을 하나씩 추가하면서 로직을 보강했다. 예를 들어 기초 이수 영역부터 전공 이수 영역까지 순차적으로 판별하는 기본 흐름을 먼저 만들고, 이후 과목 분류나 영역별 인정 조건처럼 결과에 영향을 주는 예외를 점진적으로 적용했다.

이 방식의 장점은 AI의 출력을 어느 정도 통제할 수 있다는 점이었다. AI에게 “졸업요건 검증 로직을 만들어줘”라고 요청하면 그럴듯하지만 검증하기 어려운 코드가 나올 수 있다. 반면 더미 데이터와 기대 결과, 테스트 코드를 먼저 두면 적어도 내가 확인하고 싶은 조건 안에서는 코드가 맞는지 판단할 수 있었다. AI에게 구현을 맡긴 것이 아니라, 테스트와 더미 데이터를 기준으로 AI가 벗어나지 말아야 할 울타리를 먼저 만든 셈이다.

UI 마이그레이션에 AI 활용하기

UI 마이그레이션에서도 비슷한 방식으로 접근했다. v2에서는 Mantine을 사용했지만, v2.1에서는 shadcn/Tailwind 기반으로 옮기기로 했다. 이 과정에서 먼저 비즈니스 로직과 얽혀 있는 정도를 기준으로 컴포넌트를 나눴다. 단순한 스타일, 레이아웃, 표시용 컴포넌트처럼 수정하거나 제거해도 서비스 흐름에 큰 영향을 주지 않는 부분부터 기존 디자인을 제거하고 교체했다.

반대로 Mantine에서 제공하는 hook을 사용하고 있거나, 사용자 흐름과 직접 연결된 부분은 바로 바꾸지 않았다. 이런 부분은 개별 라이브러리로 대체하거나 직접 구현하는 과정을 거쳤고, 매 단계마다 서비스 흐름이 깨지지 않는지 반복해서 확인했다. 변경하기 쉬운 것부터 바꾸고, 도메인 로직과 결합된 부분은 마지막까지 남기는 식으로 마이그레이션 범위를 조절했다.

AI 활용 후 남은 결론

이 경험을 통해 AI를 잘 쓰려면 코드 작성 능력보다도, 무엇을 바꿔도 되고 무엇을 건드리면 위험한지 구분하는 능력이 더 중요하다는 생각을 하게 됐다. 특히 Gijol처럼 도메인 규칙이 복잡한 서비스에서는 AI가 만들어낸 결과보다, 그 결과를 검증할 수 있는 테스트와 데이터, 그리고 사람이 직접 확인하는 과정이 더 중요했다.


출시 후 알게 된 사용자층의 차이

기능은 같아도 사용자 질문은 달랐다

v2.1를 만들 때 시간표, 강의 검색, 로드맵, 졸업요건 확인 기능을 함께 넣은 이유는 졸업요건 확인 이후의 행동까지 같은 서비스 안에서 이어지게 만들고 싶었기 때문이다. 하지만 출시 후 사용자 피드백을 받으면서, 같은 서비스라도 학번에 따라 사용하는 이유가 다르다는 것을 더 분명히 알게 되었다.

저학번: 앞으로의 루트를 알고 싶어 한다

저학번 학생들은 아직 수강한 강의가 많지 않기 때문에, 졸업요건을 세밀하게 확인할 필요가 상대적으로 적었다. 대신 로드맵 기능을 보며 “앞으로 이런 강의를 들어야 하는구나”, “이 전공에서는 이런 순서로 수업을 듣는구나”를 파악하는 데 더 큰 가치를 느꼈다. 실제로 로드맵을 보고 수강 계획을 세우는 데 도움이 되었다는 피드백을 받았다.

고학번: 이미 세운 계획을 검증하고 싶어 한다

반대로 고학번 학생들은 이미 자신이 설계한 수강 루트가 어느 정도 있었다. 이들에게 중요한 것은 새로운 로드맵을 탐색하는 것보다, 지금까지 들은 과목과 앞으로 들을 과목이 실제 졸업요건을 만족하는지 확인하는 일이었다. 그래서 고학번 사용자는 졸업요건 확인 기능을 주기적으로 사용하며, 자신의 계획이 안전한지 점검하는 방식으로 서비스를 사용했다.

다음 단계: 사용자층별 진입 경험

이 피드백을 통해 Gijol의 사용자를 하나의 집단으로만 보면 안 된다는 것을 알게 되었다. 저학번에게 Gijol은 앞으로의 학업 루트를 설계하는 서비스에 가깝고, 고학번에게 Gijol은 이미 세운 계획이 졸업요건을 만족하는지 검증하는 서비스에 가깝다. 다음 단계에서는 이 두 사용자층을 같은 화면에 억지로 담기보다, 각자가 가진 질문에 맞춰 다른 진입 경험을 제공해야 한다고 생각하게 되었다.


v2.1 이후 새롭게 드러난 문제

수요는 여전히 있었다

v2.1 출시 후 가장 먼저 확인한 것은, 여전히 수요가 있다는 점이었다. 짧은 개발 기간과 출시 후 기간을 고려하면 트래픽은 기대 이상으로 높게 나왔다. visitor 661명, pageview 5,736회, bounce rate 26%를 기록했고, 인당 평균 페이지뷰도 높았다. 사용자가 단순히 링크를 눌러보고 나간 것이 아니라, 여러 페이지를 이동하며 기능을 탐색했다는 신호로 볼 수 있었다.

이 결과는 Gijol이 아직 필요한 서비스라는 확신을 주었다. 특히 수강신청 기간이나 졸업요건을 확인해야 하는 시점에는 여전히 학생들이 빠르게 들어와 자신의 상태를 확인했다. v2.1에서 추가한 시간표, 로드맵, 졸업요건 확인 기능도 실제로 사용되었다. 짧은 기간 안에 다시 만든 서비스였지만, 적어도 “아직 이 문제를 필요로 하는 사용자가 있는가”라는 질문에는 그렇다고 답할 수 있었다.

공식 시스템 등장 이후의 방향 전환

동시에 환경도 바뀌고 있었다. 학교에서도 졸업 사정 시뮬레이션 기능을 제공하기 시작했다는 소식을 들었다. 그 이야기를 듣고 나니, Gijol이 단순히 졸업요건 충족 여부만 보여주는 서비스에 머물러서는 오래 수요를 유지하기 어렵겠다고 느꼈다. 공식 시스템이 제공할 수 있는 영역이 생긴다면, Gijol은 그보다 더 사용자화된 경험을 제공해야 했다. 사용자의 학번, 전공, 실제 수강 이력, 앞으로의 수강 계획을 바탕으로 더 개인화된 판단과 추천을 제공해야 한다고 생각했다.

최신 학사편람 하나로는 부족했다

v2.1에서 가장 크게 아쉬웠던 점도 여기서 드러났다. 처음에는 2025 학사편람 기준으로 강의 정보와 졸업요건 규칙을 구성하면 충분하다고 생각했다. 최신 기준을 잘 반영하면 대부분의 사용자를 처리할 수 있을 것이라고 가정했다. 하지만 출시 후 오픈채팅으로 들어온 문의를 보면서 이 가정이 틀렸다는 것을 알게 되었다.

연도별로 과목 코드가 바뀌거나, 새로 추가되거나, 사라진 강의가 있었다. 기초교육 과정 자체가 다른 과목과 과정으로 변경된 경우도 있었다. 과별로 다르게 적용되는 학점 기준이나 선이수 과목 조건도 있었고, 같은 이름의 수업이라도 연도와 맥락에 따라 다르게 간주되어야 하는 경우가 있었다. 결국 졸업요건 검증은 “최신 학사편람 하나를 기준으로 계산하는 문제”가 아니었다.

학생은 여러 해에 걸쳐 과목을 수강하고, 그 과목들은 각기 다른 시점의 학사 규칙과 과목 체계 안에 존재한다. 따라서 제대로 된 검증을 하려면 연도별 학사편람과 강의 정보를 스냅샷 형태로 관리하고, 학생이 실제로 수강한 과목의 연차와 규칙을 고려해 충족 여부를 계산한 뒤, 이를 하나의 결과로 합치는 구조가 필요하다.

다음 과제: 버전 관리와 사용자 보정

이 깨달음은 v2.1 이후의 가장 큰 과제가 되었다. 다음 단계의 Gijol은 단순히 더 많은 기능을 붙이는 방향이 아니라, 학사 규칙과 과목 정보를 버전별로 관리할 수 있는 구조를 가져야 한다. 그리고 사용자가 자신의 과목 인정 방식을 직접 보정할 수 있는 인터페이스도 필요하다. 졸업요건 검증의 어려움은 계산 자체보다, 변화하는 규칙과 예외를 어떻게 안전하게 관리하고 사용자에게 설명할 것인가에 있었다.


v1의 나와 v2.1의 나

v1: 일단 사용자에게 닿게 만든 버전

v1의 나는 일단 만들어내는 힘이 좋았다. 완벽한 구조를 따지기 전에 작동하는 것을 사용자에게 내놓았고, 그래서 학교 안에서 실제로 쓰이게 만들었다. 당시에는 개발 경험도 많지 않았고, 기술 선택의 장단점을 깊게 따질 수 있는 상태도 아니었다. 하지만 적어도 사용자가 겪는 문제를 발견하고, 그것을 서비스 형태로 빠르게 밀어붙이는 힘은 있었다.

v2.1: trade-off를 보게 된 버전

v2.1의 나는 기술 선택의 trade-off를 훨씬 더 많이 생각하게 되었다. Next.js를 쓰면 라우팅과 SSR이라는 장점이 있지만, 동시에 hydration과 상태 복원 문제를 감당해야 한다. 로그인을 붙이면 개인화 가능성이 생기지만, 인증과 사용자 관리의 책임도 생긴다. 서버를 두면 더 유연한 처리가 가능하지만, 운영 비용과 유지보수 부담도 따라온다. 이제는 어떤 기술이 좋다는 말보다, 그 기술이 이 서비스의 사용 패턴과 운영 조건에 맞는지를 먼저 보려고 한다.

AI 시대에 더 중요해진 설계

AI를 적극적으로 활용하면서 기능 구현 자체의 수고는 확실히 줄었다. 예전보다 훨씬 빠르게 화면을 만들고, 테스트를 작성하고, 로직의 초안을 구성할 수 있게 되었다. 하지만 그래서 오히려 확장 가능한 설계의 중요성을 더 크게 느끼게 되었다. 코드를 빠르게 만드는 일은 쉬워졌지만, 어떤 구조로 만들어야 예외가 늘어나도 버틸 수 있는지, 다음 사람이 이해하고 고칠 수 있는지, 도메인 규칙이 바뀌었을 때 어디를 수정해야 하는지는 여전히 사람이 판단해야 했다.

사용자 가치는 내부 구현보다 앞선다

그렇다고 v1의 방식이 틀렸다고 생각하지는 않는다. 사용자는 내부 구현을 보지 않는다. 이 서비스가 CRA로 만들어졌는지, Vite로 만들어졌는지, 서버에서 계산하는지, 프론트에서 계산하는지는 대부분의 사용자에게 중요하지 않다. 사용자가 느끼는 핵심 가치가 훼손되지 않는다면, 조금 느리거나 투박하더라도 사용할 사람은 사용한다. 반대로 내부 구조가 아무리 좋아도 사용자의 문제를 풀지 못하면 서비스로서 의미를 갖기 어렵다.

결국 균형의 문제

그래서 지금 돌아보면 v1과 v2.1 중 어느 쪽이 더 낫다고 말하기는 어렵다. v1은 사용자에게 닿게 만드는 데 성공했고, v2.1는 지속 가능하게 운영하는 방향을 고민했다. v1의 추진력이 없었다면 Gijol은 실제 사용자를 만나지 못했을 것이고, v2.1의 trade-off 감각이 없었다면 서비스는 계속 유지되기 어려웠을 것이다.

결국 좋은 서비스는 좋은 기획과 설계, 그리고 끝까지 밀어붙이는 추진력에서 나온다고 생각한다. 여기에 협업자가 함께 개발하기 편한 구조, 미래의 내가 다시 돌아와도 이해할 수 있는 구조까지 챙길 수 있다면 더 좋다. Gijol을 네 번 만들면서 바뀐 것은 이 균형을 보는 시야였다.


협업에 대한 생각

역할 분담이 명확했던 협업

Gijol에서의 협업은 운이 좋았다. 처음 팀은 백엔드, 프론트엔드, 디자인처럼 역할이 명확하게 나뉘어 있었고, 각자가 자신의 영역에 집중하면서 필요한 부분만 서로 제공하는 방식으로 진행되었다. 덕분에 큰 마찰 없이 빠르게 서비스를 만들어낼 수 있었다.

신뢰와 위임이 만든 책임감

좋았던 점은 단순히 역할이 나뉘어 있었다는 사실이 아니라, 그 역할 분담 위에 신뢰와 위임이 있었다는 점이다. 서로의 작업물을 기본적으로 신뢰했고, 각자의 영역에 대해서는 불필요하게 간섭하기보다 맡기는 분위기가 있었다. 그래서 오히려 문제가 생기면 각자가 자기 영역에서 더 책임감을 가지고 해결하려 했다. 신뢰를 받는 만큼 그 기대에 부응하고 싶어지는 협업이었다.

회의와 의사결정 방식

회의와 의사결정 방식도 잘 맞았다. 어떤 기능을 도입할 때는 “왜 이 기능이 필요한지”를 설명하고 도입하는 문화가 있었고, 회의를 오래 끌기보다는 필요한 핵심 안건을 공유한 뒤 각자 생각을 정리해와 제안하는 방식으로 진행했다. 그래서 회의가 단순한 상태 공유에 머무르지 않고, 실제 결정을 내리는 시간으로 작동했다.

투명한 공유와 피드백

개인적으로 기억에 남는 것은 작업물을 투명하게 공유하며 피드백을 자주 요청했던 경험이다. 처음에는 개발 경험이 부족했기 때문에 내가 만든 것이 맞는 방향인지 확인받고 싶었다. 그래서 결과물이 나올 때마다 공유하고, 확인해달라고 요청했다. 시간이 지나면서 팀원들이 내 작업물에 대해 긍정적인 피드백을 많이 해주었고, 그 인정은 다시 더 잘 준비하고 더 책임감 있게 작업하려는 동력이 되었다. 기대와 노력이 서로를 강화하는 선순환이 있었다.

아쉬웠던 점

물론 아쉬움도 있었다. 역할 분담이 명확했던 만큼 각자의 영역 안에서 빠르게 작업할 수 있었지만, 반대로 같은 영역 안에서의 코드리뷰나 지식 공유는 상대적으로 부족했다. 당시에는 서비스를 빠르게 출시하는 것이 우선이었기 때문에 자연스러운 선택이었지만, 돌아보면 코드 공유와 리뷰 문화가 조금 더 있었다면 팀 전체의 학습 속도도 더 빨라졌을 것 같다.

다음 협업에서 가져가고 싶은 원칙

그래서 다음 협업에서는 명확한 역할 분담과 빠른 실행력은 유지하되, 일정에 부담을 주지 않는 선에서 코드리뷰나 작업 공유 방식을 Working Agreement에 포함하고 싶다. 좋은 협업은 진심만으로 이루어지지 않는다. 무엇을 만들지, 누가 어디까지 책임질지, 어떤 기준으로 서로의 작업을 공유하고 검토할지를 먼저 합의해야 신뢰와 위임이 제대로 작동한다.


마무리 — 무엇을 가져갈 것인가

바뀐 것은 기술이 아니라 기준이었다

같은 서비스를 4년에 걸쳐 네 번 만들면서 가장 크게 바뀐 것은 코드도, 디자인 라이브러리도 아니었다. “무엇이 좋은 서비스인가”에 대한 내 정의가 바뀌었다.

v1에서 좋은 서비스는 작동하고 사람들이 쓰는 것이었다. v2에서 좋은 서비스는 사용자가 진짜 서비스라고 느낄 수 있는 것이었다. v2.1에서 좋은 서비스는 운영자가 무리하지 않고도 지속될 수 있는 것이었다. 그리고 지금은 여기에 하나가 더해졌다. 좋은 서비스는 변화하는 도메인 규칙과 사용자 예외를 감당할 수 있는 구조를 가져야 한다.

다음 버전에 대한 생각

다음 버전이 있다면 부전공과 복수전공의 더 완전한 지원, 학번별·연도별 스냅샷 모델, 졸업요건 룰셋을 관리할 수 있는 어드민, 그리고 사용자가 직접 자신의 과목 인정 방식을 보정할 수 있는 구조가 들어가야 한다고 생각한다. 더 나아가 학교 내부 인증 시스템과 연계하고, 학업 계획에서 연구실 탐색, 취업 준비까지 이어지는 학생 의사결정 지원 서비스로 확장해보고 싶다.

오래 붙잡은 문제에서 배운 것

결국 좋은 서비스는 한 번에 만들어지지 않았다. 같은 문제를 다시 바라볼 때마다 조금씩 다르게 보였고, 그 차이가 다음 버전의 이유가 되었다. Gijol은 내게 졸업요건 확인 서비스이기 이전에, 사용자의 문제를 오래 붙잡고 다시 정의하는 법을 배우게 해준 프로젝트였다.