01 / OVERVIEW
어떤 프로젝트인가요?
N-GLIS는 서울아산병원의 유전체 검사·분석 결과를 관리하고 시각화하는 의료 시스템입니다. 저는 프론트엔드 팀에서 시퀀싱(Sequencing) 영역을 맡아, 장비 QC 데이터를 시각화하는 D3 차트 5종과 관련 화면, 전역 상태 설계, E2E 테스트를 개발했습니다.
차트는 시퀀싱 장비 회사(Illumina)의 분석 도구를 판독 기준으로 삼았습니다. 장비가 만들어내는 원시 품질 데이터를, 검사자가 익숙한 방식 그대로 읽을 수 있는 화면으로 옮기는 작업이었습니다. 의료 도메인이라 "보기 좋은 차트"가 아니라 "기준 도구와 같은 방식으로 읽히는 차트"가 요구사항이었습니다.
02 / DEEP DIVE
핵심 기능 집중 소개
01
Flow Cell 차트 — 타일 번호에서 물리 배치 복원
어떤 고민이 있었는가
- 백엔드는 타일 번호(예: 11101)와 측정값만 보냅니다. 화면은 이 번호를 장비 셀의 물리 배치(lane · surface · swath · section)로 되살려 장비 기준 도구(Illumina SAV)와 같은 모양의 히트맵으로 그려야 했습니다.
- 일부 타일이 빠지면 격자가 무너졌고, 지표에 따라 값 범위가 0.4에서 16,544까지 달라 고정된 색 스케일로는 분포가 보이지 않았습니다.
어떻게 해결했는가
- Start: 백엔드에서 타일 번호와 측정값 목록을 받는다(예: 11101).
- 타일 마스터 목록과 대조해 빠진 타일은 값 0으로 채워 격자를 유지한다.
- 타일 번호를 자릿수로 분해한다 — surface = ÷10000, swath = ÷1000, section = %100. surface 2면 x좌표에 swath 수를 더해 뒷면을 오른쪽에 놓는다.
- 지표의 최소·최대로 7단계 임계값 색 스케일을 만들고 히트맵을 그린다.
- 컬러바의 임계값 손잡이를 d3.drag로 끌면 값 범위에 따라 감도를 6단계(0.0002 ~ 5)로 바꿔 적용하고, 전체 lane의 셀 색을 다시 계산한다.
- 타일 번호를 자릿수로 분해해 surface(÷10000) · swath(÷1000) · section(%100)으로 해석하고, 뒷면(surface 2)은 x좌표에 swath 수를 더해 앞면 옆에 놓는 매핑을 구현했습니다. 타일 마스터 목록과 대조해 빠진 타일은 값 0으로 채워 격자를 유지합니다.
- 지표의 최소 · 최대로 7단계 임계값 색 스케일을 만들고, 컬러바의 손잡이 7개를 d3.drag로 직접 끌 수 있게 했습니다. 값 범위에 따라 드래그 감도를 6단계(0.0002 → 0.01 → 0.1 → 1 → 2 → 5)로 바꿔, 소수점 지표와 만 단위 지표 어느 쪽에서도 미세 조정이 됩니다.
결과적으로
- 장비 기준 도구와 같은 방식으로 읽히는 Flow Cell 히트맵을 구현했고, 손잡이를 끌면 전체 lane의 셀 색이 즉시 다시 계산됩니다.
02
Data by Cycle — 318사이클 라인 · 박스플롯 이중 렌더링
어떤 고민이 있었는가
- 사이클(최대 318)별 품질 흐름을 두 방식으로 봐야 했습니다 — 채널 전체를 비교할 때는 라인 그래프, 채널 하나를 고르면 타일 분포를 박스플롯으로.
- 검사자는 마우스 위치의 값을 모든 채널에서 동시에 읽어야 하고, 지금 보는 구간이 어느 read(R1~R4)인지도 알아야 합니다.
어떻게 해결했는가
- 필터 선택에 따라 라인 그래프와 박스플롯을 전환하는 이중 렌더링을 구현하고, 채널 수(2채널 · 4염기)에 맞춰 선을 동적으로 생성했습니다.
- x.invert 기반 크로스헤어로 마우스 위치의 전 채널 값과 범례를 동시에 갱신하고, R1~R4 read 경계를 점선과 라벨로 표시했습니다.
결과적으로
- 기준 도구의 판독 방식을 그대로 재현한 사이클 차트를 제공하며, 같은 컴포넌트가 Summary 화면에서도 고정 지표 모드로 재사용됩니다.
03
차트 5종을 두 화면에서 재사용
어떤 고민이 있었는가
- Flow Cell, Data by Cycle, QScore 히트맵 · 분포, Data by Lane까지 같은 차트 5종이 런 상세 화면(URL 파라미터 기준)과 분석 화면(선택한 프로젝트 기준) 두 곳에서 필요했습니다.
어떻게 해결했는가
- 차트를 크기 props만 받는 컴포넌트로 유지하고, 데이터 식별자는 "URL 파라미터 ?? Redux 선택값" 한 줄 규칙으로 해석하게 해 다섯 차트가 모두 같은 방식으로 두 화면을 공유합니다.
결과적으로
- 차트 5종을 중복 구현 없이 두 화면에 배치했고, 분석 화면에서 프로젝트를 바꾸면 전체 차트가 함께 갱신됩니다.
04
새로고침을 견디는 공통 Store 설계
어떤 고민이 있었는가
- 선택한 프로젝트 · 샘플 · 목록 필터는 새로고침 후에도 유지돼야 했지만, 이동 경로(breadcrumb) 같은 휘발성 상태까지 남으면 잘못된 화면을 보여주게 됩니다.
어떻게 해결했는가
- Start: 화면 조작으로 Redux 상태가 바뀐다 — 선택한 프로젝트 · 샘플 · 목록 필터, 그리고 이동 경로(breadcrumb).
- 리듀서가 blacklist(sequencingBreadCrumb)에 있으면 메모리에만 두고, 아니면 Redux Persist가 저장소(AsyncStorage)에 쓴다.
- 새로고침하면 REHYDRATE가 저장된 상태를 읽고, autoMergeLevel2가 초기 상태 위에 두 단계 깊이로 병합한다. breadcrumb은 초기값으로 시작한다.
- 차트 5종은 데이터 식별자를 "URL 파라미터 ?? Redux 선택값" 순서로 해석해, 런 상세(URL)와 분석 화면(선택값) 두 곳에서 같은 구현을 쓴다.
- Redux Toolkit과 Redux Persist로 공통 Store를 설계하고, autoMergeLevel2 병합 정책에 휘발성 리듀서 blacklist(breadcrumb)를 조합해 "남길 상태"와 "버릴 상태"를 구분했습니다. 타입 지정된 RootState · AppDispatch도 함께 정리해 모든 화면이 같은 훅을 씁니다.
결과적으로
- 새로고침해도 작업 맥락이 유지되고, 휘발성 상태로 인한 화면 오염이 사라졌습니다.
05
비가역 입력을 지키는 UX와 Cypress E2E
어떤 고민이 있었는가
- Flow Cell ID는 한 번 입력하면 수정할 수 없는 값이라, 그리드에서의 입력 실수를 반드시 막아야 했습니다.
어떻게 해결했는가
- Start: 검사자가 시퀀싱 목록 그리드에서 Flow Cell ID 셀을 편집하고 저장한다.
- 편집 커밋(processRowUpdate)이 Promise 기반 확인 모달을 띄운다 — "한 번 입력된 Flow Cell ID는 수정이 불가합니다. 진행하시겠습니까?"
- 취소하면 편집을 되돌린다. 확인하면 PUT API로 저장한다.
- 저장에 성공하면 그 행의 편집 아이콘을 숨겨 다시 고칠 수 없게 잠근다. 실패하면 "입력에 실패했습니다. 관리자에게 문의해주세요." 모달을 띄운다.
- 같은 그리드의 값 표기는 런 상태(PLANNED · SEQUENCING · COMPLETED)에 따라 분기한다. Cypress Page Object(updateFlowCellId · changeSequencingStatus)가 이 흐름을 회귀 검증한다.
- DataGrid의 편집 커밋(processRowUpdate)에 Promise 기반 확인 모달을 끼워 넣어 "한 번 입력된 Flow Cell ID는 수정이 불가합니다"를 확인받은 뒤에만 저장하고, 저장된 행은 편집 아이콘을 숨겨 잠갔습니다. 저장 실패는 별도 모달로 안내하고, 런 상태(PLANNED · SEQUENCING · COMPLETED)에 따라 값 표기를 분기했습니다.
- Cypress E2E를 Page Object 패턴(updateFlowCellId · changeSequencingStatus)으로 작성해 Flow Cell ID 입력과 상태 변경 흐름을 자동 검증합니다.
결과적으로
- 비가역 입력의 실수를 UI 단계에서 차단하고, 핵심 사용자 흐름을 회귀 테스트로 보호합니다.
03 / SCREENS
서비스 화면
04 / REFLECTION
배운 점
의료 도메인에서는 "익숙한 판독 방식"이 곧 요구사항이었습니다. 기준 도구의 규칙을 코드로 옮기면서, 시각화는 디자인 문제가 아니라 도메인 해석 문제라는 것을 배웠습니다.