포트폴리오에 있는 '자세히 보기' 팝업만으로는 Apice.kr이라는 프로젝트에 어떤 기술이 들어가 있는지 온전히 전달하기가 아무래도 어려웠어요. 몇 줄짜리 요약으로는 왜 세션과 토큰을 굳이 같이 쓰기로 했는지, 승인 구조를 왜 두 단계로 나눴는지까지는 담을 수가 없더라고요. 그래서 이번 글에서는 팝업에 못다 적은 이야기까지 포함해서, Apice.kr의 기술 스택을 처음부터 끝까지 자세히 풀어보려고 해요.
전체 구조, 한눈에 보기
Apice.kr은 인천상정고등학교 Apice동아리의 실험계획서 등록과 참여 신청, 그리고 선생님 승인까지 이어지는 흐름을 하나의 서비스로 옮긴 프로젝트예요. Flask 위에서 화면을 그려주는 페이지 라우트와, 데이터를 주고받는 JSON API 라우트가 함께 동작하는 구조로 되어 있어요.
데이터는 SQLite에 저장하는데, 그냥 기본 설정으로 쓰지는 않았어요. PRAGMA journal_mode=WAL과 busy_timeout 30초 설정을 함께 적용해서, 여러 학생이 동시에 신청 버튼을 눌러도 "database is locked" 같은 에러 없이 안정적으로 처리되도록 만들었어요.
테이블은 크게 다섯 가지로 나눠져 있어요.
- users — 아이디, 비밀번호, 이름, 학년·반·번호, 역할(학생/선생님) 정보
- experiments — 실험계획서 본문, 정원, 모집 마감일, 총 회차 수, 승인 여부
- experiment_sessions — 실험계획서 하나에 딸린 회차별 일정(날짜·시작 시간·종료 시간·내용)
- participants — 실험 참여 신청자와 승인 상태(대기/승인/거절)
- notifications — 승인 결과를 알려주는 알림
혹시 이전에 상정인사이드 기술 스택 글을 보셨다면 아시겠지만, 그 프로젝트가 철저한 '익명성'을 지향했던 것과는 정반대로 Apice.kr은 학생 한 명 한 명의 신원이 학년·반·번호 단위로 명확하게 확인되어야 하는 서비스예요. 그래서 이번 프로젝트에서는 어뷰징 방지보다, 인증 방식과 승인 흐름을 얼마나 꼼꼼하게 설계하느냐에 훨씬 더 공을 들였어요.
인증 · 권한 — 세션과 토큰을 함께 쓰는 이유
두 갈래로 로그인 상태를 지키기
로그인에 성공하면 서버는 두 가지를 동시에 만들어요. 하나는 브라우저에 저장되는 세션 쿠키이고, 다른 하나는 7일짜리 JWT 토큰이에요. 쿠키에는 HttpOnly, Secure, SameSite=Lax 옵션을 기본으로 걸어뒀고, 토큰은 HS256 알고리즘으로 서명해서 내려줘요.
이렇게 두 가지를 같이 쓰는 이유는 요청이 들어오는 경로가 하나가 아니기 때문이에요. login_required 데코레이터는 먼저 세션을 확인하고, 세션이 없으면 Authorization: Bearer 헤더에 담긴 JWT를 검증한 뒤, 토큰이 유효하면 그 정보로 세션을 다시 채워 넣어요. 덕분에 쿠키 기반으로 페이지를 넘나드는 일반적인 흐름과, 토큰만 들고 API를 직접 호출하는 흐름을 같은 로그인 로직 하나로 함께 처리할 수 있어요.
권한에 따라 다른 화면
역할은 student와 teacher 두 가지로 나뉘는데, 관리자 기능 전체는 teacher_required 데코레이터로 한 번 더 감싸져 있어요. 로그인은 했지만 선생님 권한이 없는 사람이 관리자 페이지에 접근하면, 그냥 403 에러를 던지는 대신 "선생님 전용 페이지입니다"라는 안내와 로그인 페이지 링크가 담긴 전용 화면을 보여주도록 만들었어요.
CSRF와 요청 속도 제한
Flask-WTF의 CSRFProtect를 앱에 걸어뒀고, 프론트엔드가 토큰을 받아갈 수 있도록 /api/csrf-token 엔드포인트도 따로 만들어 뒀어요. 여기에 더해 Flask-Limiter로 로그인, 로그아웃, 비밀번호 변경, 메인 페이지 같은 주요 경로에 IP 기준 초당 요청 수 제한을 걸어서, 스크립트로 반복 요청을 보내는 상황에 대비했어요. 제한에 걸리면 기본 429 에러 화면 대신, 동아리 브랜드에 맞춘 안내 페이지가 뜨도록 별도의 에러 핸들러를 만들어 뒀어요.
실험계획서 등록과 두 단계 승인 구조 — 가장 공들인 부분
회차별 일정을 가진 실험계획서
하나의 실험계획서는 한 번의 활동으로 끝나지 않는 경우가 많아요. 그래서 실험계획서 본문(experiments)과 회차별 일정(experiment_sessions)을 별도 테이블로 분리해서, 하나의 실험이 여러 회차의 날짜·시간·내용을 각각 가질 수 있도록 설계했어요.
일정 충돌은 등록 시점부터 걸러내기
실험계획서를 등록할 때는 모집 마감일이 실험 일정보다 뒤에 오지는 않는지, 회차들이 시간 순서대로 배치되어 있는지를 서버에서 먼저 검증해요. 여기에 더해 /api/check-time-conflict API를 따로 만들어서, 이미 승인된 다른 실험들의 일정과 시간대가 겹치지는 않는지도 확인할 수 있게 했어요. 같은 시간에 여러 실험이 동시에 잡히는 상황을 등록 단계에서부터 막기 위한 장치예요.
정원과 승인, 두 번의 확인
승인은 한 번이 아니라 두 단계로 나눠져 있어요.
- 실험계획서 승인 — 학생이 작성한 실험계획서 자체를 선생님이 검토하고, 승인해야만 목록에 노출돼요.
- 참여자 승인 — 승인된 실험에 다른 학생이 참여를 신청하면, 그 신청 건도 선생님이 한 번 더 승인해야 확정돼요.
정원 체크는 학생이 신청하는 시점과 선생님이 승인하는 시점, 이렇게 두 번 이뤄져요. 신청 이후 다른 신청 건이 먼저 승인되면서 정원이 먼저 차 버리는 경우까지 감안한 처리예요. 그리고 실험계획서를 작성한 학생 본인은 별도로 신청하지 않아도 자동으로 승인된 참여자 목록에 포함되도록 만들어 뒀어요.
알림 시스템 — 승인 결과를 놓치지 않도록
실험계획서가 승인되거나 참여 신청이 승인될 때마다, notifications 테이블에 알림이 하나씩 쌓여요. 사용자는 /api/notifications에서 최근 10건을 확인할 수 있고, 확인한 알림은 읽음 처리를 해서 다음에 다시 확인할 때 구분할 수 있도록 만들었어요. 승인 결과를 확인하려고 매번 관리자에게 따로 물어보지 않아도 되게끔 만든 기능이에요.
최초 로그인은 비밀번호 변경부터
학생 계정은 선생님이 직접 만들어요. 아이디·이름·학년·반·번호와 함께 초기 비밀번호를 정해서 등록하면, 해당 계정에는 first_login 값이 1로 표시돼요. 이 값이 1인 계정으로 처음 로그인하면 다른 페이지로 넘어가기 전에 /change-password 화면을 먼저 거치도록 만들어서, 선생님이 정해준 초기 비밀번호를 그대로 계속 쓰는 일이 없도록 했어요. 한 번 비밀번호를 바꾸고 나면 first_login이 0으로 바뀌면서, 그다음부터는 이 과정 없이 바로 서비스를 이용할 수 있어요.
실패도 브랜드에 맞게 — 에러 페이지와 엣지 케이스
없는 페이지에 접속했을 때(404), 요청이 몰려서 제한에 걸렸을 때(429), 잘못된 하위 도메인으로 접속했을 때까지 — 각각의 상황마다 기본 에러 화면이 아니라 동아리 로고와 다크 테마가 적용된 전용 안내 페이지를 보여주도록 만들었어요. 존재하지 않는 하위 도메인으로 접속하더라도 그냥 에러가 뜨는 대신, 올바른 주소인 www.apice.kr로 안내하는 페이지로 자연스럽게 연결되도록 처리했고요.
PWA로 확장하기
/manifest.json 라우트에서 앱 이름, 아이콘, 테마 색상, standalone 표시 모드를 담은 웹 앱 매니페스트를 내려줘서, 모바일에서 Apice.kr을 홈 화면에 추가하면 설치된 앱처럼 열리도록 만들었어요. 여기에 /server.js를 정적 파일로 함께 서빙해서, 매니페스트만으로는 부족한 나머지 PWA 동작을 보완하도록 했어요.
사용자가 직접 체감할 수 있는 UX · UI
지금까지는 눈에 보이지 않는 곳에서 벌어지는 이야기였다면, 이제부터는 실제로 사용자가 화면을 통해 마주하게 되는 기능들을 소개해 드릴게요.
① 첫 화면, 그리고 두 개의 입구

로그인 여부와 상관없이 누구나 가장 먼저 마주하는 화면이에요. 상단은 동아리 로고와 로그인 버튼만 남겨서 최대한 단순하게 비워두고, 가운데에는 "인천상정고등학교 Apice동아리 공식 홈페이지"라는 문구로 이 사이트가 무엇을 하는 곳인지부터 분명하게 알리고 싶었어요.
그 아래 놓인 "실험 제안하기"와 "실험 참여하기" 버튼은 각각 새 실험계획서를 등록하는 흐름과, 이미 올라와 있는 실험에 참여를 신청하는 흐름으로 바로 이어져요. 로그인을 하지 않은 상태에서도 "내가 지금 뭘 하러 왔는지"에 따라 갈 길을 스스로 고를 수 있게 만들고 싶어서, 첫 화면의 선택지를 이 두 가지로 압축했어요.
② 같은 톤으로 맞춘 입력 화면

로그인 화면과 실험계획서 작성 화면이에요. 두 화면 모두 왼쪽 위에 "← 돌아가기"로 빠져나갈 길을 먼저 열어두고, 큰 제목 아래에 이 화면에서 뭘 해야 하는지 한 줄로 짚어주는 구조를 그대로 맞췄어요. 로그인 화면에는 "Apice동아리 · 실험 관리 시스템"이라는 문구를 카드 위에 한 번 더 얹어서, 로그인이 단순한 인증 절차가 아니라 이 서비스 전체를 대표하는 진입점이라는 느낌을 주고 싶었어요.
실험계획서 작성 화면은 "1, 2학년 여러분의 실험 계획을 공유해주세요"라는 문장으로 누가 이 글을 쓰는 화면인지부터 짚어주고, 제목·설명 입력란에는 "예: DNA 추출 실험"처럼 실제로 쓸 법한 예시를 placeholder로 넣어서 빈 화면 앞에서 무엇부터 적어야 할지 막막해하지 않도록 했어요. 여기서 저장을 누르면 앞서 설명드린 승인 대기 상태로 들어가고, 선생님이 승인해야만 다른 학생들에게 목록으로 노출되는 구조예요.
③ 승인 권한을 세 갈래로 나누기

선생님 계정으로 로그인하면 만나는 화면이에요. 상단 탭이 "실험계획서 승인" · "참여자 승인" · "학생 계정 관리" · "모든 실험 관리" 이렇게 네 갈래로 명확히 나뉘어 있는데, 이게 앞서 설명드린 두 단계 승인 구조를 화면에 그대로 옮겨놓은 형태예요. 실험계획서 자체를 승인하는 일과, 이미 승인된 실험에 들어온 신청을 승인하는 일을 한 화면에 뭉쳐두지 않고 탭으로 갈라서, 지금 내가 어떤 종류의 승인을 처리하고 있는지 헷갈리지 않게 했어요.
④ 승인하자마자, 화면 위로 바로 뜨는 결과

바로 앞 화면에서 "DNA 추출 실험"을 승인 버튼으로 처리하고 나면 뜨는 화면이에요. 페이지를 새로고침하거나 별도 알림함을 열어보지 않아도, 처리한 결과가 초록색 배너로 화면 위에 바로 올라와요. 앞서 말씀드린 notifications 테이블에 기록이 쌓이는 순간을, 관리자 쪽에서도 "방금 내가 누른 승인이 제대로 반영됐다"는 즉각적인 피드백으로 확인할 수 있게 만든 거예요.
⑤ 목록에서 상세보기까지, 한 번에 보이는 정보

목록 카드에는 모집 인원 대비 신청 현황과 모집 상태와 실험을 등록한 작성자를, 상세보기에는 마감까지 남은 시간을 실시간으로 계산해서 보여줘요. 내가 만든 실험에는 "내가 만든 실험" 태그를 붙이고 "수정은 선생님께 문의하세요"라고 안내해서, 학생이 직접 고칠 수 있는 경로가 없다는 걸 헷갈리지 않게 했어요.
실험 자세히 보기에서는 실험 설명과 실험 일정 그리고 참여자 목록까지 한 화면에서 모두 확인할 수 있도록 구성했어요. 추가로 닫기 버튼 왼쪽에 공유하기 버튼을 두어서, 친구에게 링크를 보내서 참여를 권유할 수도 있게 만들었어요.