Skip to content

현재 사용법 (2026-09-10, 6fbaee7f): FE 성능·측정·기존 동작 영상 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.

스케줄러 첫 화면 로딩 — 직렬 왕복 11회를 2회로 ​

  • 티켓: CS-LOC-260909-0001 「스케줄러 화면 로딩이 너무 오래 걸립니다」 (분당베스트병원, admin /admin/#scheduler)
  • 브랜치: fix/cs-scheduler-load-time

요약 ​

관리자 화면의 「스케줄러」 메뉴를 열었을 때 근무표가 뜨기까지 기다리는 시간이 절반 이하로 줄어듭니다. 지연 150ms 를 흉내 낸 로컬 측정에서 근무표 표시까지 2.4초가 1.2초로, 서버 요청 묶음은 16회가 8회로 줄었습니다. 병동이 많은 병원일수록 차이가 더 커집니다.

근본 원인은 두 가지가 겹친 것입니다. 첫째, 스케줄러 컴포넌트의 초기화 함수가 두 번 호출되어 모든 초기 조회가 두 번씩 나갔습니다. 둘째, 서로 의존하지 않는 조회들을 하나씩 차례로 기다려서 서버 왕복이 11번 직렬로 이어졌습니다.

배경 — 지금 동작 ​

「스케줄러」는 admin 안에 격리된 Alpine 컴포넌트(apps/admin/src/scheduler.ts, 마크업 views/scheduler/scheduler.html)로 붙어 있습니다. 화면이 마운트되면 init() 이 실행되어 필요한 데이터를 서버에서 읽어 옵니다. 서버 호출은 tRPC 의 httpBatchLink 를 쓰므로, 같은 틱에 함께 시작한 호출은 HTTP 요청 하나로 묶여 나가고, await 로 하나씩 기다리면 각각 별도의 왕복이 됩니다. 이하 서버에 한 번 다녀오는 것을 「왕복」이라고 씁니다.

init() 은 다음 순서로 조회했습니다. 프로필(간호사 여부 확인) → 활성 탭 데이터 → 팀 목록 → 팀원 목록 → 스케줄러 설정 → 병동 목록. 활성 탭이 근무표(scheduler)이면 그 안에서 다시 설정 → 병동 → 병동 설정 → 근무 유형 → 오늘 환자수 → 병원 합계(전 병동 프로필 + 병동마다 환자수 1회씩) → 근무표 순으로 이어졌고, 근무표 조회(loadSchedule) 안에서도 휴일 → 프로필·근무표 목록 → 근무표 상세·준수율 검증 순이었습니다. 로컬에서 실측한 결과 근무표 관련 요청 묶음만 11개가 직렬로 이어졌습니다.

여기에 마크업이 x-data="schedulerComponent()" x-init="init()" 로 되어 있었습니다. Alpine 은 x-data 객체에 init() 이 있으면 자동으로 호출하므로, x-init="init()" 은 같은 함수를 한 번 더 부르는 것입니다. 원본인 team-manager 의 컴포넌트에는 booted 재진입 가드가 있었으나 admin 으로 옮기면서 빠졌고, 그 결과 위 조회 사슬 전체가 두 벌씩 동시에 돌았습니다(설정·병동 조회는 4번).

개념 다이어그램 ​

변경 전

변경 후

개념 흐름별 변경 ​

초기화가 한 번만 돌게 합니다. scheduler.html 에서 x-init="init()" 을 제거하고, scheduler.ts 의 init() 앞에 booted 가드를 둡니다. 가드는 마크업이 다시 x-init 을 얻더라도 두 번째 호출을 무시하는 2차 방어선입니다.

의존이 없는 조회는 같은 왕복에 싣습니다. init() 의 첫 왕복은 프로필 확인·스케줄러 설정·병동 목록 세 가지를 Promise.all 로 함께 보냅니다. 병동 목록만은 일반 간호사 계정에서 소속 병동으로 걸러야 하고 그 기준(myWardIds)이 프로필 확인 결과에 있으므로, 병동 조회를 fetchWardList(네트워크)와 applyWardList(필터·기본 선택)로 나누어 셋이 모두 돌아온 뒤 적용합니다. 팀 목록·팀원 목록은 admin 스케줄러 화면에서 쓰지 않으므로 그리드를 막지 않게 뒤에서 따로 돕니다. 근무표 탭의 loadTabData 는 병동 설정·근무 유형·오늘 환자수·병원 합계·근무표를 한 Promise.all 로 보내고, 병원 합계 안의 병동별 환자수 조회도 for … await 에서 Promise.all 로 바꿉니다. 병동 설정 안에서 순서대로 부르던 근무 유형 조회도 같이 묶습니다.

근무표는 한 번에 받습니다. 기존에는 schedule.list(limit 1) 로 id 를 얻은 뒤 schedule.get(id) 로 entries 를 다시 받았습니다. 새 조회 schedule.findByPeriod({ wardId, year, month }) 가 최신 근무표 1건을 entries 까지 포함해 돌려주므로(없으면 null) 두 왕복이 하나가 됩니다. 정렬 기준은 list 의 기본값과 같은 createdAt DESC 이고, get 과 마찬가지로 entries 의 employee 는 싣지 않습니다. 스키마는 packages/core 의 findNurseScheduleByPeriodSchema, SDK 메서드는 findScheduleByPeriod 입니다. 휴일 캐시 적재(ensureHolidayYears)도 이 배치에 함께 넣되, 달력 상태(cycleDates)는 배치가 끝난 뒤 세팅하여 렌더 시점에 휴일 캐시가 채워져 있다는 기존 보장을 유지합니다.

준수율 검증은 그리드 뒤에 따라옵니다. entry.validate 는 준수율 배지 하나를 위한 호출이므로 근무표 표시를 막지 않게 뒤에서 호출하고, 그 사이 월이나 병동이 바뀌어 다른 근무표가 떠 있으면 scheduleId 를 대조해 결과를 버립니다.

첫 왕복 동안의 깜빡임을 정리합니다. loading 초기값이 false 라서 첫 왕복 동안 「간호사가 등록되지 않았습니다」가 잠깐 떴다가 스피너로 바뀌었고, 간호사 계정은 프로필 확인이 끝나기 전까지 「권한 없음」 안내가 떴습니다. loading 을 true 로 시작하고, 권한 안내는 roleChecked 가 된 뒤에만 보입니다.

확인한 것 ​

  • pnpm lint:check · pnpm build (전체) · admin Vitest 215건 통과.
  • findByPeriod 와 기존 get 이 같은 근무표에 대해 같은 entries 수(376)를 돌려주고, 근무표가 없는 달은 null 을 돌려줍니다.
  • Playwright 로 #scheduler 진입을 실측: 요청 묶음 16 → 8, 근무표 관련 직렬 왕복 11 → 2. 근무표가 있는 달(2026-07)로 바꾸면 entries·커스텀 근무 라벨·준수율(56%)이 표시됩니다.