buildrisk-radar

ADR-030 프론트엔드를 화면 · Hook · 순수 함수 · 기능별 API 모듈로, 경계는 ESLint 로

배경 화면 파일이 API 경로를 직접 조립하고(40여 곳) 변경 요청을 직접 보냈으며(20여 곳), 정렬 · 판정 · 변환 같은 규칙이 컴포넌트 안에 섞여 있었다. 조회 Hook 은 경로가 바뀌어도 이전 응답을 돌려줘 새 이름표 옆에 옛 숫자가 보였고(문제 140), 화면 여럿이 조회 오류를 그리지 않아 무한 로딩이 됐고(141), 응답 순서 경합 · 처리되지 않은 거부 · 남는 오류가 있었다(142).

결정

  1. 네 층 (의존은 위에서 아래로만)

    층 하는 일
    pages · components 그리기와 화면 상태(탭 · 펼침 · 입력값)
    lib/hooks 상태 · 부수효과 — useApi(조회) · useAction(변경 요청) · usePolling(주기 호출) · useEventNames · useSiteConfig(화면 설정)
    lib/api 기능별 모듈(company · region · alert · insight · notify · ops · session)이 경로 · 메서드 · 응답 타입을 한 곳에, client 가 fetch · CSRF · 경로 검사. config 는 Next 의 /api/config(런타임 화면 설정)
    lib/domain React 없는 순수 함수 — 표기 · 색 기준 · 변환 · 판정 (단위 테스트)
  2. 경계는 ESLint 로: 화면은 lib/api/client · fetch 금지(기능 모듈만). lib/domain 은 React · next · 화면 · 네트워크 · 브라우저 전역 금지, API 는 응답 타입만 import type.
  3. 조회 응답은 경로에 묶음: data 는 지금 경로의 값만, 이전 조건의 값은 stale 로 따로(성적표처럼 흐리게 남길 곳만). 경로가 바뀌거나 화면을 떠나면 요청 취소. 상태 전이는 순수 함수(requestState)라 React 없이 시험.
  4. 변경 요청은 useAction(시작할 때 이전 오류를 지우고 실패는 오류로 담음, 진행 중 잠금), 주기 호출은 usePolling(탭이 보일 때만).
  5. 효과 안에서 setState 하지 않음(react-hooks/set-state-in-effect 켬) — 브라우저에만 있는 값은 useSyncExternalStore, 나머지는 상태에서 파생.

다르게 둔 곳과 이유

근거 (2026-10-09)

덧붙임 (2026-10-09, 독립 검토 뒤 — 문제 160~167) 목록 화면도 상세 화면과 같은 표시 순서(값이 없을 때의 오류 → 불러오는 중 → 없음 → 내용)로 맞췄고 — 같은 조건을 다시 부르다 실패하면 보이던 값은 남김(163 · 167), 값을 스스로 들고 있는 지도 컴포넌트는 새 값을 기다리는 동안 툴팁을 띄우지 않는다(162 — 140 과 같은 종류가 ‘경로에 묶인 응답’ 밖에 남아 있었음). 지도 E2E 는 카카오 SDK 대역(폴리곤 이벤트 · 툴팁 상태를 기록하는 작은 스크립트)으로 외부 망 · 키 없이 돈다. 단위 테스트 39 · E2E 47개.

다시 볼 때

조회 캐시 · 재시도 · 낙관적 갱신이 필요해지면 그때 라이브러리를 검토한다. 기능 모듈 하나가 여러 화면의 공통 묶음을 키우면 다시 나눈다 (같은 측정으로 확인).

덧붙임 (2026-10-11, VERIFICATION 203) — 경계 규칙의 빈틈 lib/domain 규칙이 @/ 경로만 봐서 ../api/client 같은 상대 경로가 통과했고, 화면의 fetch 금지가 이름 fetch 만 봐서 window.fetch · globalThis.fetch 가 통과했다. lib/domain 은 상위 상대 경로를 금지하고, 세 객체(window · globalThis · self)의 fetch 속성을 금지한다. 우회 모양은 lib/boundaries.test.ts 가 ESLint 로 직접 검사해 고정한다(백엔드 ArchitectureTest 의 ‘우회하기 쉬운 모양’과 같은 역할).