코어 엔진 번들의 정규식 lookbehind 2건이 Safari 16.4 미만에서 파싱 자체를
거부당해 iOS 15 대 전 기기에서 SPA 가 통째로 뜨지 않았다. 정규식 리터럴은
스크립트 파싱 단계에서 검증되므로 그 문법을 모르는 엔진은 파일을 한 줄도
실행하지 못하고, 번들이 async/defer 없는 동기 classic 스크립트라 폴백 경로도
없었다. 빌드 타깃(es2020)이 이를 막지 못한 이유는 ES 연도와 브라우저 지원
연도가 다르기 때문이며, 정규식 리터럴 문법은 다운레벨 자체가 불가능해
어떤 타깃 설정으로도 걸러지지 않는다.
- 두 정규식을 lookbehind 없는 형태로 교체(engine-v1.60.6). 선행 따옴표·구분자를
소비한 뒤 캡처 그룹으로 분기하며, 식별자 인덱스가 match[1] → match[2] 로
이동한다. 무작위 200,034 표본 퍼징에서 교체 전후 결과 불일치 0건.
파싱 하한 실측 Safari 16.4 → 14.1 (제보자 기기 15.6 포함).
- 부팅 실패 안내를 사유별로 가른다. 번들을 받았는데 실행되지 않은 경우와
끝내 받지 못한 경우는 사용자가 취할 행동이 정반대인데 같은 문구였다.
사유는 추측이 아니라 window error 이벤트로 SyntaxError 를 관측해 판정한다
(파싱 실패 시 script 는 error 가 아니라 load 를 발생시켜 element 이벤트로는
구분 불가). 비호환 분기는 새로고침해도 낫지 않으므로 버튼을 렌더하지 않는다.
인라인 부트스트랩은 ES5 를 유지해야 이 안내 자체가 구형에서 살아남는다.
- 재발 방지: 배포 JS 산출물의 브라우저 하한 초과 문법·API 를 정적 검사로 차단.
부팅 임계 번들(코어 엔진·템플릿 컴포넌트)은 파싱에 실패하면 안내 화면조차
렌더되지 않으므로 하한과 같은 버전을 요구하는 정규식 리터럴 문법도 금지한다.
lookbehind 의 Safari 16.4 는 하한과 정확히 같아 하한 초과만 보는 검사로는
영원히 잡히지 않았다 — 그 지점이 이번 사각이었다.
- 지원 브라우저 최소 버전(Chrome 111 / Safari 16.4 / Firefox 128)과, 하한 미만
브라우저에서도 화면 표시와 기본 이용은 유지한다는 방침을 문서에 명시.
부수 수정: network-resilience E2E 4건이 자산 URL extensionless 모드에서 URL
glob 이 한 건도 매칭되지 않아 실패하고 있었다. 같은 파일의 컴포넌트 번들
테스트만 이중 모드를 알고 형제 케이스는 몰랐던 불균형이라 매처를 통일했다.
요구사항 문서가 "지원한다"고 선언한 항목과 실제로 검증되는 범위가 어긋나 있었다.
지원 브라우저 3종 "최신 2개 버전" 선언에 비해 자동 테스트는 Chromium 단일이었고,
같은 형태의 불일치가 문서 전반에 있었다 — 디스크 700MB↔게이트 500MB, 확장 33개
필수 선언↔게이트 14개, HTTPS "필수"↔비차단 경고.
선언을 낮추는 대신 검증 수준을 명시하는 방향으로 정리했다. 항목마다 "설치 프로그램이
자동 검사" 와 "운영자 확인 필요" 를 구분하는 절(§9)을 두어, 이슈가 지적한 질문
("지원 선언이 어느 수준의 검증을 의미하는가")을 문서 전반으로 일반화했다.
조사 중 드러난 내부 하네스 결함 3건도 함께 고쳤다.
- 테스트 스코프 훅이 npm run test:e2e:smoke 를 오차단 — 필터가 package.json 안에
있어 명령 문자열에서 보이지 않았다. 회귀 케이스 3건 red 확인 후 수정
- 브라우저 테스트 안내 문서가 없어진 스크립트·디렉토리를 가리키고 있었다
- E2E 동반 강제 룰의 확장 config 경로 패턴이 실제 경로와 달라, 확장 8개의 config
변경이 인정되지 않았다
E2E 브라우저 확대는 하지 않는다 — 도입 조건만 문서에 못박았다.
공개 제보 — 파일 스토리지에서 S3 를 선택해 저장해도 실제 파일 저장이
동작하지 않던 결함의 전면 수정.
- S3 어댑터(league/flysystem-aws-s3-v3)·predis 를 코어 기본 의존성으로 포함
— 어댑터 부재 즉사, phpredis 확장 없는 서버의 redis 선택 전면 다운 차단
(부트 시 확장 부재 감지 → predis 자동 폴백)
- storage_driver=s3 저장 시 코어 첨부 업로드 디스크를 s3 로 전환
(ATTACHMENT_DISK env 명시가 항상 우선, 기존 행은 저장 당시 disk 로 서빙)
- 첨부·템플릿 레이아웃 첨부 서빙을 행 disk 를 따르는 스토리지 스트림으로 교체
— 로컬 절대 경로 전제 fileResponse 는 S3 행에서 filemtime stat 500
(streamedFileResponse: 행 메타 기반 ETag/304/Cache-Control)
- S3 호환 스토리지(R2/MinIO/NCP) 연결 지원: 엔드포인트 URL·path-style 설정
신설, 리전 목록 선택 → 자유 입력 전환, 연결 테스트를 실제 저장 경로와
동일 설정(endpoint/path-style)으로 정렬
- 사용 불능 드라이버(어댑터·PHP 확장 부재)의 저장/테스트 요청을 사유와 함께
422 로 차단하는 서버 게이트 신설 (DriverRegistryService 능력 판정)
- 웹소켓 연결 테스트에 서버(백엔드 발송용) endpoint 검사 추가 — 클라이언트만
검사해 테스트 성공 + 실제 발송 실패가 가능하던 비대칭 해소
- env 빈 값(`KEY=`) 함정 정규화: AWS_URL/AWS_ENDPOINT/ATTACHMENT_DISK 빈 문자열을
미설정으로 취급 (config 정규화 + 예시 파일 주석 처리)
- 플러그인 드라이버 폴백의 log 카테고리 죽은 키(logging.default) 정정 및
websocket 유령 설정 키 제거
- 실 AWS S3 종단 검증 완료 (설정 저장 → 업로드 S3 실저장 → 서빙 200/304)
공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다.
응답·행의 무게를 다룬 · 과 겹치지 않는다.
공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이
기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은
per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지
번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로
정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은
계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다.
계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면
그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다.
응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새
필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate
응답은 필드 단위로 이전과 동일하다.
요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일
재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어
화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한
변경이 종전처럼 다음 요청에 반영된다.
검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한
엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어
활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩
대신 선언형 config 로 옮겼다.
총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을
돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.