Skip to content

Owner·Auth·Connectivity 수명주기 분리 — 세대 정본 하나·떠나는 순서 계약·인증/연결 runtime 을 controller 밖으로 — v0.18.0 A07 (2026-09-07)

  • 기간: 2026-09-07 (세션 2개 — d161e4b2 Phase 0~4·Phase 5 문서 초안, 세션 한도로 중단 → 3140ad75 이어받아 리베이스·장부·ci:local·PR·머지. 오너 지시 "#1335 진행해주고, 혹시 선행작업 안끝났으면 끝나고 진행해줘" — 선행 A01 947413d1·B01 ba0f4035 반영 확인 뒤 착수). 계획 ID A07 / Phase 2 Step 2-2. 총괄 A07로 돌아가기.
  • 랜딩: PR #1365 → main 0775e6b2(2026-09-08 00:5x KST squash, CI verify 범위 초록) · staging Deploy run 34137394668 성공(resolve·database·functions·frontend·smoke 전부 초록, release tag 는 Production 전용이라 skip) · 앞수리 PR #1362 4f2dc691(ci:local) · 이 기록의 마무리는 docs PR(아래 등록 뒤 머지). (Phase 1~4 커밋 3fd2e9ba·d508dc6a·7fdcd364·834f0a0e — origin/main 632ccab3(S03 #1363 뒤) 위로 리베이스한 해시. 리베이스 전 2d1add5d·40ecf2e4·7aa11550·90a2fdd8. Phase 5 9cb01bd1·0fff8aee·1bbdfc92·da87fa3d+기록 갱신) — 마이그레이션·엣지 함수 없음, 랜딩 큐 불필요(일반 절차). 앱 controller 변경이라 main 머지 = staging 배포, Production 은 v0.18.0 릴리스. 머지 해시·staging run 은 위에 적었다.
  • 설계서: 이슈 #1335 착수 댓글(연결도·해결 방안·Phase 5개·예상 효과·개선사항 표).
  • 정본: Owner·Auth·Connectivity 수명주기 계약 · src/react/app/{ownerRuntime,ownerStoreSetHost,authSessionRuntime,connectivityRuntimeHost}.ts · 변환 어댑터 controllers/authSessionRevision.ts.
  • 도구: 없음(레포 안 스크립트 변경 없음). 이동은 세션 scratchpad 의 추출 스크립트(레포 밖)로 자리만 옮겼다.
  • 게이트: 새 테스트 4파일 22건(tests/react/ownerRuntimeownerStoreSetHostauthSessionRuntime 5 + 픽스처 경유 ownerSwitchFixture·authSessionFailurePolicy 행동 전환) · 픽스처 tests/support/ownerSwitch.mjs(제품 runtime 으로 세대)·authSessionRuntime.mjs · npm run check(Phase 마다) · npm run ci:local --full(Phase 5, 검증 줄은 PR 본문) · 명부 신고 5건(pending-changes.json: appContainer·backgroundTokens·controllerBoundaries·logTableMonthCache 이어 적기, authSessionFailurePolicy 새 항목) · 경계 검사 2곳·계층 허용표에 src/react/app/** 등재.
  • 버그리포트: 없음(오너 보고 결함이 아니라 계획된 구조 개선).
  • 계약: 새 문서 docs/architecture/owner-auth-connectivity-lifecycle.md §1~§8. v0-18-0-layer-dependency-table.md 에 앱 runtime 행 추가. types/controllers.ts AuthStateResetter 에 reason 인자.

Phase 현황

Phase내용상태
Phase 0분석·연결도(이슈 댓글)
Phase 1OwnerRuntime — 세대·취소 신호·자원 등록/폐기·떠나는 순서의 정본 한 곳3fd2e9ba(구 2d1add5d)
Phase 2owner 전환 순서 계약 — 세트 교체를 runtime 전환 안으로, owner 취소 신호를 transport 까지d508dc6a(구 40ecf2e4)
Phase 3AuthSessionRuntime — 인증 부팅·복원·이벤트·재시도를 controller 밖으로, 세 사건에 이름7fdcd364(구 7aa11550)
Phase 4ConnectivityRuntime host — 연결 조립을 controller 밖으로, 폐기를 owner 자원으로834f0a0e(구 90a2fdd8)
Phase 5계약 문서·G05 장부·작업 기록·ci:local·PR·CI·머지·staging 확인9cb01bd1 0fff8aee 1bbdfc92 da87fa3d c4de8de8 → 머지 0775e6b2 · staging run 34137394668 성공

1. 배경

v0.18.0 Phase 2 는 "안전한 저장 경로와 첫 수직 통합" 이다. 그중 A07 은 계정(owner)·인증·연결의 수명주기를 feature 조회에서 떼어 내, 다음 스텝 A08(resource cache)이 "이 응답을 지금 화면에 적용해도 되는가" 를 owner 세대 하나로 판정할 수 있게 하는 자리다. A01 이 transport 에 취소 신호 자리를 만들어 두었고("A07 가 owner 떠날 때 abort 할 signal 을 제공"), B01 이 인증 오류 분류와 보존 정책 표를 A07 인계로 남겼으며, S08 이 초안 보존 순서(prepareWorkoutDraftForOwnerChange·보존 꼬리)를 완성해 두었다.

2. 문제 제기

같은 뜻의 번호표 4개가 각자 셌다

유저 A 가 조회 중 로그아웃하고 B 가 로그인하면 "이 응답은 지금 주인 것인가" 를 검사해 버려야 한다. 그 기준값이 authSessionRevision(controller 내부)·ownerScopeController.remoteUserRevisionRef(렌더 중 증가, 14개 파일이 (userId, revision) 쌍을 손으로 들고 다님)·authSessionLifecycleRevision·authBootstrapRevision 네 곳에 있었다. G03 OwnerScope{userId, epoch} 는 이름만 있고 구현이 없었다.

떠나는 순서가 두 곳에 나뉘어 뒤바뀌어 있었다

adoptAuthenticatedIdentity 가 scope 무효화와 ref 리셋 60여 줄을 먼저 돌리고, React 가 B 화면을 그린 뒤 layoutEffect 의 adoptOwnerStores 가 A 의 초안 보존을 돌렸다 — 무효화가 보존보다 먼저였다. 보존의 완료/실패를 기다릴 port 는 S08 의 모듈 꼬리뿐이었다.

자원 정리가 등록/폐기가 아니라 목록이었다

resetRemoteUserData 가 ref 를 하나씩 비우고, 연결 runtime 폐기는 effect cleanup 과 이 함수 두 곳, 부팅 재시도 타이머 취소는 세 함수가 각각 했다. 새 화면을 추가할 때마다 목록에 줄을 더해야 했고, 빠뜨리면 B 화면에 A 데이터가 남는다.

인증·연결 조립이 4,091줄 controller 의 effect 두 개(약 700줄)에 갇혀 있었다

React 없이 검사할 수 없어 테스트 2건이 소스 문자열 앵커로 검사했다. 세 사건(만료·명시 로그아웃·계정 삭제)은 보존 정책은 지켜지고 있었지만 runtime 에 "왜 떠났는가" 라는 이름이 없었다.

3. 해결 방안

원칙 (오너 결정)

이번 트랙에 오너 결정은 없다. 전제: "#1335 진행해줘" 를 착수 승인으로 봤다(이슈 범위는 총괄·HQ 승인 완료, 일상 착수 재승인 불필요). 보존 정책은 B01 계약 §5 표 그대로, 제품 정책 값 변경 0.

접근

내용판정
리셋 목록 보강·검사 위치 추가빠진 ref 를 찾아 목록에 넣고 조회마다 scope 검사 추가기각 — 다음 화면이 또 빠뜨린다(전역 지침 §22)
전역 singleton 하나에 인증·연결·owner 전부한 store 가 모든 상태 소유기각 — 이슈·총괄이 금지(feature 성공/실패를 인증 상태로 합치지 않는다, 전역 mutable singleton 회귀 금지)
작은 runtime 3개 + owner 전환 계약 (채택)OwnerRuntime(세대·자원·순서), AuthSessionRuntime(인증 증거·세 사건), ConnectivityRuntime host(기존 runtime 을 owner 자원으로). 기존 store 세트·연결 runtime 본체는 그대로 두고 누가 언제 부르는지만 한 곳으로채택

4. 적용한 내용

Phase 1 — OwnerRuntime (2d1add5d)

  • src/react/app/ownerRuntime.ts: OwnerScope{userId, epoch} 구현. adopt/leave(reason)/capture/isCurrent/signal/register(preserve|dispose)/subscribe/awaitLeave/inspect. 떠날 때 보존 → 폐기(등록 역순) → epoch+1·abort → 알림. 같은 A 재로그인 = 새 세대.
  • authSessionRevision.ts 는 runtime 위의 변환 어댑터(revision = epoch), 제거 조건 명시. ownerScopeController 는 렌더 중 카운팅을 버리고 runtime epoch 를 읽는다. appController 가 셸 인스턴스당 runtime 하나를 만들어 주입.
  • 경계 검사 2곳(frontendImportBoundaries·contractsImportBoundaries)과 계층 허용표에 src/react/app/** 를 controllers 와 같은 규칙으로 등재.

Phase 2 — owner 전환 순서 계약 (40ecf2e4)

  • src/react/app/ownerStoreSetHost.ts: store 세트 슬롯을 runtime 에 붙인다 — ① preserveOwnerStores(S08 보존 꼬리를 결과로) ② resetOwnerStores ③ epoch·abort ④ 새 세트. appController layoutEffect 는 세트를 읽기만.
  • ownerStores.ts: leaveOwnerStores 를 preserve/reset 두 단계로 나눔(종전 진입점·adoptOwnerStores 유지).
  • 셸 hydration·PR 대시보드·볼륨·세션 상세 요청에 owner 취소 신호 연결(리스너는 요청 끝에 해제).

Phase 3 — AuthSessionRuntime (7aa11550)

  • src/react/app/authSessionRuntime.ts: controller 의 인증 effect 408줄을 자리만 옮겨 start/stop/retryBootstrap 으로(host = 참조·콜백·API 표면 주입). 상수 4개 정본 이동.
  • 세 사건: SIGNED_OUT → transitionToSignedOut("", "signed_out"), 만료 문구 → expired, profileController.deleteAccount 가 서버 성공 뒤 deleted. AuthStateResetter 에 reason. B01 분류기를 API port 로 소비.
  • 하네스 tests/support/authSessionRuntime.mjs + 행동 테스트 5건. authSessionFailurePolicy 앵커 1건 → 행동 검사.

Phase 4 — ConnectivityRuntime host (90a2fdd8)

  • src/react/app/connectivityRuntimeHost.ts: 연결 effect 278줄을 자리만 옮겨 owner scope 하나에 묶인 host(runtime + dispose 반복 안전). 본체 controllers/connectivityRuntime.ts 불변.
  • controller effect 는 host 를 만들고 ownerRuntime.register(() => host.dispose()). 고아 import 16개 정리.

Phase 5 — 문서·장부·랜딩

  • 계약 문서 docs/architecture/owner-auth-connectivity-lifecycle.md(§1 유저 이야기·§2 runtime 3개·§3 세대와 변환 경계·§4 순서·§5 취소 규칙·§6 세 사건·§7 데이터≠인증·§8 A08 인계), G05 장부(auth controller/route·shell controller/binding 행), 이 기록, 사이드바·README 등록.
  • npm run ci:local --full → PR → CI 1회 → 머지 → staging 확인(결과는 PR 본문·이 문서 §5).

주요 결정과 그 근거

  • 세대는 runtime 하나가 세고, 기존 {revision, userId} 는 이름 바꾸기 어댑터로 남긴다: 14개 파일을 한 PR 에서 옮기면 A08 과 같은 파일을 동시에 고치게 된다. 숫자·의미가 같으므로 섞어 비교해도 안전하고, 제거 조건("import 0")을 문서에 적어 두 카운터가 독립적으로 자라는 것을 막는다.
  • 보존을 렌더 전(동기)으로 당긴다: 초안 env 는 이전 세트에 렌더마다 붙어 있으므로 auth 이벤트 시점에 보존해도 최신 스냅샷이다. 보존 실패는 전환을 막지 않는다(마지막 기기 저장물이 복원 기준, S08).
  • 첫 로그인('' → A)도 세트를 바꾼다: 종전 지휘자 동작과 같다 — 로그인 전 화면 상태(토스트·내비)가 첫 owner 로 새지 않게.
  • resetRemoteUserDatadisposeConnectivityRuntime() 은 남긴다: owner 자원 폐기가 먼저 돌아 no-op 이지만 안전망. 목록 자체는 A08 이 resource cache 로 흡수한다.
  • "자리만 이동": 인증·연결 effect 는 free variable 을 host 주입으로 바꾼 것 외에 로직을 바꾸지 않았다(기존 any 도 그대로, boundary gate 허용 목록에 사유 등재). 행동 변경은 세 사건 reason 과 취소 신호 연결뿐.

작업 중 드러난 것

  • 소스 앵커 테스트가 많다: 세트 교체 위치 변경에 3파일, 인증 effect 이동에 5파일, 연결 effect 이동에 2파일이 걸렸다. sourceSliceAnchors.test.mjs(앵커 레지스트리)가 "어느 앵커가 어느 파일에서 사라졌는가" 를 정확히 알려 줘 재조준이 기계적이었다. 명부 등재분은 pending-changes.json 기존 항목에 이어 적었다(새 항목은 중복 거부 — B01 교훈).
  • 부팅 순서기의 재시도 간격(250ms·500ms)은 실제 타이머라 "5xx 는 degraded" 행동 테스트는 실제 시간을 900ms 기다린다. 테스트마다 t.after(() => runtime.stop()) 이 없으면 재시도 타이머가 프로세스를 붙들어 테스트 러너가 멈춘다(처음 120초 타임아웃으로 발견).
  • 큰 python 히어독은 Bash 툴에서 따옴표가 깨진다 — 추출·재조준 스크립트는 파일로 쓰고 python file.py.
  • appContainer 의 부팅 채택 기록: 기기 흔적 채택 1 + 순서기의 local·verified 채택 2 = adopt 3회 뒤 adoptVerified — 종전과 같은 횟수(행동 불변).
  • 사전 검증 누락 1건(세션 1 → 2 인계 뒤 발견): 브라우저 회귀 픽스처 tests/react/authResume.browser.mjs 가 controller 를 옛 인자(owner runtime 없이)로 불러 window.authResume 이 만들어지지 않았다 — ci:local persistence 묶음 3건 시간 초과. Phase 마다 npm run check(단위)만 돌리고 브라우저 묶음은 Phase 5 로 미뤘기 때문에 Phase 1 에서 잡을 수 있었던 것을 Phase 5 에서 잡았다. 조립 루트와 같이 createOwnerRuntime() 주입으로 수리(c8b19681). 교훈: controller hook 의 필수 인자를 바꾸면 grep useRemoteDataController( 로 픽스처까지 전수 확인.
  • main 앞수리 1건: B02(#1356) 뒤 ci:local 이 verify 묶음 전체에 BARBELIC_TARGET=local 을 줘 관리자 셸 빌드까지 local 로 새겨지고 check:artifacts 가 항상 실패했다(CI 는 build 단계에만 준다). 별도 PR #1362 4f2dc691build 단계에만 주게 고쳤다(CI 생략 — 로컬 전용 스크립트, 계약 테스트 27건 로컬 통과).
  • G05 장부 JSON 을 json.dump 로 다시 쓰면 안 된다: 규칙 파일이 펼친 객체·한 줄 객체가 섞인 모양이라 통째로 직렬화하면 2,240줄 diff 가 난다(세션 1 이 그렇게 남김). HEAD 로 되돌린 뒤 문자열 치환으로 행 4개·규칙 3줄만 고치고 --render(미분류 0).
  • Bash 히어독은 백슬래시를 먹는다(정규식 \\.md) — JSON 규칙 한 줄도 Edit 도구로 써야 했다(메모리 bash-heredoc-escape-mangling 재확인).
  • 포트 4173 경합: 다른 세션의 미리보기가 4173 을 잡고 있어 ci:local --full 이 browser 단계에서 코드 3. --only verify,db,local,empty,cardio,persistence 로 먼저 돌리고, 같은 빌드를 4179 미리보기(샌드박스 E2E_SUPABASE_* 를 준 프로세스)에 띄워 CI=true E2E_APP_URL 로 browser·viewport 를 따로 돌렸다.
  • CASE-004 자정 걸침 플레이크: browser 묶음이 23:55 KST 에 시작해 자정을 넘겼고 CASE-004(하루 요약 RPC 대기)만 1회 실패 뒤 재시도 통과 — 메모리의 8/31 기록(23:56 시작 → 자정 넘김)과 같은 모양. 자정 뒤(00:10 KST) 같은 미리보기에서 재시도 0 으로 3회 반복 → 3/3 통과(38초). 코드 원인 아님.

5. 적용 결과

항목전 → 후
같은 뜻의 세대 카운터4 → 1(OwnerRuntime.epoch; authSessionRevision·remoteUserRevisionRef 는 이 값을 읽는 어댑터). 부팅 비행 번호·인증 이벤트 순번은 세대가 아니라 남김(계약 §3)
떠나는 순서무효화 → (렌더) → 보존 → 새 세트 → 보존 → 폐기 → 무효화 → 새 세트(동기, 렌더 전). 보존 완료/실패 port 0 → 1(awaitLeave)
A 의 늦은 성공/실패가 B 화면·cache·toast·초안에 적용행동 테스트 0건 적용(ownerRuntime·ownerStoreSetHost·ownerSwitchFixture), A→A 재로그인 새 세대에서 옛 결과 0건 채택
owner 취소 신호 → transport없음(A01 §5 미구현) → 셸 hydration·PR·볼륨·세션 상세 4경로. 나머지 조회는 scope 비교 유지(A08 대상)
연결 runtime 폐기 지점effect cleanup + resetRemoteUserData 2 → owner 자원 등록 1(+ 안전망 no-op)
세 사건reason 없음 → signed_out/expired/deleted/switched, B01 §5 표와 1:1
데이터 5xx → 인증 상태 변화(검사 없음) → 0건(행동 테스트)
remoteDataController.ts4,091 → 3,503줄(인증 408·연결 278 이동, 고아 import 정리). src/react/app/** 4파일 1,217줄 신설
소스 앵커 검사 → 행동 검사2 → 1건 전환(authSessionFailurePolicy SIGNED_OUT 경계). 늦은 401 hydration 앵커는 controller 에 남아 유지
전체 게이트단위 3,070건 통과(Phase 4) · tsc·lint·unused·boundary gate·test-manifest·앵커 레지스트리 통과 · 검증: ci:local full · verify 통과 (8단계) · db reset(마이그레이션 전체 적용) 통과 · schema.sql 스냅샷 --check 통과 · pgTAP 통과 117파일/2036 assert · 동시 저장 세대·영수증 통과 · e2e-local 통과 11/11 · e2e-empty 통과 7/7 · e2e-cardio 통과 6/6 · e2e-persistence 통과 11/11 · 4분 42초 · browser 37/38 통과 + CASE-004 1회 재시도 통과(자정 걸침) · viewport 14/14
실기기·브라우저ci:local --full 의 브라우저 묶음(로그인·계정 전환 여정 포함)까지 자동 검증. 시각·감각적 품질은 사람 눈 몫(정보)
main/stagingPR #1365 → main 0775e6b2 · staging Deploy run 34137394668 성공(database·functions·frontend·smoke) · Production 은 v0.18.0 릴리스 대기

6. 이번 개선으로 향상된 것

공용 기기에서 계정을 바꿔도 이전 사람의 화면이 새지 않는다

A 의 늦은 응답은 B 의 화면·cache·toast·초안 어디에도 닿지 않고, 같은 A 가 다시 로그인해도 로그아웃 전 결과는 버려진다. 서버 요청 자체도 owner 가 떠나면 끊긴다.

초안 보존이 무효화보다 먼저다

A 가 기록 중 로그아웃해도 마지막 편집이 먼저 기기에 저장되기 시작하고, 곧바로 다시 로그인하면 그 저장이 끝난 뒤 복원한다.

새 화면·새 조회가 시작할 자리가 있다

세대 캡처/비교(capture·isCurrent), owner 종료 신호(signal·subscribe·reason), 자원 등록/해제(register) — 리셋 목록에 줄을 더하는 대신 이 셋을 쓴다. A08 은 이 위에 resource cache 를 올린다.

구조적으로 남는 것

OwnerScope 구현과 변환 어댑터의 제거 조건, 떠나는 순서 계약과 그 테스트, 세 사건 reason, React 없이 도는 인증·연결 runtime 하네스, 계약 문서 §8 의 A08 인계.

남은 것

  • A08(#1338): resetRemoteUserData 의 손 리셋 약 50 ref 를 ResourceCachePort.invalidateOwner 로 흡수, 피드·검색·카탈로그·연도 활동·PR 상세 조회에 취소 신호·OwnerScope 키 적용, AuthRequestScope 소비자 축소(어댑터 제거 조건).
  • 릴리스 v0.18.0 뒤 [v0.18.0 반영완료] + 이슈 닫기.