게시물뿐 아니라 그 앞의 과목 조회도 두 번 실행됐다
LearningX 대시보드에서 같은 조건의 API가 두 번 호출되는 문제가 보고됐다. 교수자 게시물 요청만 중복된 것이 아니었다. 과목, 위험학생, 할 일 요청이 함께 반복됐고 학생 화면에서도 같은 현상이 있었다.
이 화면은 학생이면서 조교인 계정도 지원한다. 접속 시 저장된 역할과 학기를 복원하고, 현재 역할의 과목을 조회한 뒤 그 과목들의 데이터를 가져온다. 역할과 학기가 준비되는 순서를 정하지 않으면 중간 상태도 정상 조회 조건처럼 사용될 수 있었다.
당시 분석은 selectedRole과 selectedTerms의 갱신 시점에 주목했다. 두 값이 동시에 바뀌지 않아 과목 조회 effect가 각각 실행되고, 새 과목 목록을 읽는 후속 effect도 다시 실행됐다.
역할별 API 분리는 초기화 순서까지 정해주지 않았다
기존에는 역할 설정, 학기·사용자 설정 조회, 과목 조회, 역할별 후속 조회가 별도 effect에 있었다. 과목 조회는 역할과 선택 학기를 의존성으로 삼았고, 후속 조회는 과목 데이터의 변화를 보고 시작했다. 각 effect는 자신의 데이터만 보면 필요한 반응을 하고 있었지만 초기 복원 과정에서는 그 반응이 중복 요청으로 이어졌다.
요청을 병렬로 보내는 것만으로 해결할 수 있는 문제도 아니었다. 앞선 변경에서 이미 학생의 공지·할 일과 교수자의 게시물·위험학생·할 일은 Promise.all로 묶었다. 그 묶음이 두 번 시작되면 병렬화한 요청도 두 번 실행된다.
수정은 같은 요청의 응답을 캐시하거나 모든 요청을 중복 제거하는 계층을 추가하지 않았다. 초기 조건을 정리하는 단계와 그 조건으로 데이터를 조회하는 단계를 나눴다.
초기 조건을 정리한 다음 한 흐름에서 조회했다
첫 단계는 사용자 학기를 읽고 계정 설정과 배너를 가져온 뒤 학기 데이터를 세팅한다. 다음 단계에서 역할이 하나인 계정은 그 역할을 선택한다. 저장된 선택 학기 중 현재 목록에 있는 것만 남기고, 남은 선택이 없으면 기본 학기를 사용한다. 선택 배열이 달라질 때만 갱신한 뒤 isInitialized를 켠다.
데이터 조회는 이 플래그를 확인하는 fetchAllData로 합쳤다. 아래는 실제 변경에서 요청 시작을 막는 분기다.
if (!isInitialized || selectedTerms.length === 0) {
setCourseData([]);
setLearnerTodoData([]);
setInstructorTodoData([]);
setAnnouncementData([]);
setPostData([]);
setRiskLearnerData([]);
return;
} - 초기 입력 복원학기·계정 설정을 읽고 사용할 역할과 학기를 정리한다.
- 과목 조회현재 역할과 선택 학기에 맞는 과목을 가져오고 blueprint 과목을 제외한다.
- 역할별 후속 조회남은 과목 ID로 현재 화면에 필요한 API 묶음만 실행한다.
과목이 없으면 공지·할 일·게시물·위험학생 데이터를 비우고 후속 요청을 하지 않는다. 과목이 있으면 학생은 공지와 할 일을, 교수자는 게시물·위험학생·할 일을 병렬로 읽는다. 위험학생 기능이 비활성일 때는 빈 배열 Promise로 그 요청을 건너뛴다.
이 흐름은 하나의 try/finally에서 loading을 관리한다. 과목 조회가 끝난 뒤 별도 후속 effect가 다시 로딩을 여는 구성을 합친 것이다. effect의 의존성은 초기화 플래그, 역할, 선택 학기다. 이후 사용자가 역할이나 학기를 바꾸는 것은 필요한 재조회로 남는다.
초기화는 서버 요청이 가능해졌다는 한 가지 조건만 뜻하지 않는다. 화면이 사용할 역할과 학기를 정리하는 단계가 먼저 있어야 했다. 다만 이 플래그가 모든 상태 갱신과 응답을 하나의 원자적 작업으로 만드는 것은 아니다.
중복 초기 조회를 다뤘지만 모든 경합을 막지는 않았다
필수 초기 입력을 기다리면 데이터를 빨리 요청하는 기회는 늦어진다. 당시 초기 함수는 학기, 설정, 배너를 순서대로 기다렸다. 준비 중인 상태로 조회하지 않는 선택에는 이 시작 지연이 있다. 배너까지 같은 초기 흐름에 둔 것이 모든 화면에 필요한 일반 원칙은 아니다.
| 조건 | 확인한 구현 | 남은 범위 |
|---|---|---|
| 초기화 전·빈 학기 | 데이터를 비우고 조회하지 않음 | 모든 재접속에서 한 번만 호출한다는 보장은 아님 |
| 후속 API 일부 실패 | Promise.all의 실패를 공통 catch에서 처리 | 성공한 일부 결과를 따로 반영하지 않음 |
| 빠른 역할·학기 전환 | 조건 변경으로 다시 조회 | 오래된 요청 취소·응답 무효화는 없음 |
특히 Promise.all 안의 하나가 실패하면 뒤의 결과 반영이 생략된다. 나머지 요청 자체를 취소하는 것도 아니다. 빠르게 조건을 바꿔 이전 요청과 다음 요청이 겹치는 상황에는 AbortController나 응답 순서 확인이 이 커밋에 없다. 이전 응답이 늦게 도착하는 경합까지 해결했다고 말할 수 없다.
공통 loading boolean도 여러 조회가 겹친 상황의 실행 수를 세지는 않는다. 이번 변경은 초기 복원 중 시작되는 조회의 경계를 정리한 것이다. 일반적인 exactly-once 실행이나 모든 역할 전환의 최신 응답 보장과는 다른 범위다.
코드 분할도 별도 작업이었다. 과목별 보기 컴포넌트의 React.lazy 도입은 중복 데이터 요청을 수정한 뒤 별도로 진행했다. 화면 코드를 언제 가져올지와 데이터 요청을 언제 시작할지는 따로 결정해야 한다.
정적인 요청 횟수와 초기 상태 전이 검증은 다르다
기존 메인 컴포넌트 Jest 테스트는 학기가 없거나 과목이 비면 후속 요청이 없는지, 역할에 맞는 API가 호출되는지, 위험학생 옵션과 오류 조건이 동작하는지 검사한다. 일부 호출에는 toHaveBeenCalledTimes(1)도 있다.
그러나 그 테스트는 API와 설정 훅을 모킹하고 역할·학기를 정적인 값으로 준다. 기본 상태에서 서버 설정으로 복원되는 실제 전이를 재현한 시험으로 볼 수 없다. 수정에 새 테스트가 추가된 것도 아니다. 파일에 있는 검증 범위와 당시 이중 호출의 회귀 검증을 구분해야 한다.
대시보드의 역할별 조회는 필요한 데이터만 나누는 데서 시작했지만, 이후에는 그 조회를 시작해도 되는 상태를 분명히 해야 했다. 요청 하나를 빠르게 만드는 것과 같은 일을 두 번 시작하지 않는 것은 서로 다른 문제였다.