검색/브라우저, 서버, 네이티브에서 모두 제대로 동작하는 CSS-in-JS 라이브러리 유지보수하기

브라우저, 서버, 네이티브에서 모두 제대로 동작하는 CSS-in-JS 라이브러리 유지보수하기

AI 코딩 도우미로 표준을 따르는 CSS-in-JS 라이브러리를 작업할 때 쓴다. 스타일은 표준 CSS만 쓰게 하고, 각 런타임에서 코드가 깨지지 않게 하며, 사양을 바탕으로 테스트를 먼저 쓰고, 릴리스 노트·문서·실제 코드를 돌리는 데모를 만드는 방법을 정한다.

이 AI 규칙을 사용하려면
## 스타일에는 표준 CSS만 받고, 설정한 기본값을 우선하기 작성되는 스타일 표면을 실제 표준 준수 CSS와 정렬된 상태로 유지한다. 이렇게 하면 사고 모델을 그대로 이식할 수 있고, API가 독자적인 방언으로 흘러가는 것을 막을 수 있다. - 작성되는 스타일에서는 표준 속성명, 표기, 값만 허용한다. 비표준 별칭(예: 플랫폼 전용 prop을 위한 케밥 케이스 이름)을 절대 만들지 않는다. 플랫폼별 스타일 키는 템플릿 언어가 아니라 명시적인 탈출구(인라인 스타일 객체, 또는 업스트림 플랫폼 지원)에 속한다. - 네이티브 레이어의 역할은 관용적인(idiomatic) CSS를 동등한 플랫폼 프리미티브로 매핑하고, 네이티브 런타임이 따라잡을 때까지 가능한 부분을 폴리필하는 것이다. 허용되는 입력 문법을 확장하기 위한 것이 아니다. ### 설정된 기본값의 우선순위 - 작성자가 설정한 속성 기본값은 설계상 항상 호출자가 전달한 prop보다 우선한다. 이는 버그가 아니라 의도된 계약이다. - 탈출구는 defaults 설정의 함수 형태다. 이 함수는 들어오는 props를 받아 조건부로 대체 처리를 할 수 있다(호출자의 값을 유지하거나 기본값을 제공한다). - 예외: 호출자가 어떤 prop에 명시적으로 `undefined`를 전달하면, 해당 prop에 대해 설정된 기본값은 적용되지 않는다. ## 프로젝트가 정한 패키지 매니저와 커밋 형식 쓰기 - 매 세션을 시작할 때는 현재의 사용자 대상 기능 요약을 읽어, 기억에 의존한 동작이 아니라 오늘 시점에 지원되는 표면을 기준으로 작업한다. - 프로젝트가 지정한 패키지 매니저와 그에 연관된 명령어를 사용한다. 다른 것으로 대체하지 않는다. - 개발 서버를 직접 실행하지 않는다. 필요하면 메인테이너에게 실행을 요청한다. - 커밋 메시지에는 컨벤셔널 커밋 접두사(`feat|fix|chore|refactor|test|docs|style|perf|build|ci`)를 사용한다. 기본적으로 짧게 작성한다. 제목만 쓰거나, 제목에 간결한 문장 하나를 덧붙인다. - 모든 산문(주석, 문서, 테스트 제목, 사용자에게 노출되는 문자열)에서는 하나의 영어 철자 규칙을 일관되게 사용한다. 원래 철자는 사양이나 표준에서 그대로 인용한 부분에서만 유지한다. ## 서버, 브라우저, 네이티브를 구분하고 네이티브에 DOM 코드를 넣지 않기 여러 런타임에 배포되는 코드는 특정 환경 하나를 전제해서는 안 된다. 여러 독립적인 신호로부터 환경을 감지하고, 호환되지 않는 코드는 진입점(entry-point) 경계에서 격리한다. - 환경 감지를 하나의 범용 플래그로 뭉뚱그리지 않는다. 최소한 다음을 구분해야 한다. 빌드 타임 서버 상수(서버 빌드에서만 true), 런타임 서버 컴포넌트 조건(일부 프레임워크는 클라이언트 전용 컨텍스트 API가 없는 빌드를 제공한다), SSR 진입점에서 설정되는 요청 단위 서버 플래그. 각 판단은 자신이 대응해야 하는 시나리오를 포착하는 데 필요한 최소한의 신호 집합에만 근거해야 한다. 흔한 함정: DOM 관련 작업을 순전히 빌드 타임 서버 상수만으로 억제하면, DOM 변경이 여전히 일어나는 jsdom 기반 서버 빌드 테스트 경로가 깨진다. DOM 관련 판단은 런타임/SSR 신호로 게이트해야 한다. - 네이티브 진입점은 값 임포트(value import)를 통해 DOM/브라우저 코드를 간접적으로라도 절대 임포트해서는 안 된다. `instanceof` 대신 타입 전용 임포트와 브랜드된 런타임 태그(공유 심볼 레지스트리)를 사용한다. 빌드된 네이티브 번들에서 `document.` 참조가 0건인지 grep으로 검증한다. 일부 네이티브 런타임은 `document` 참조가 하나라도 있으면 모듈 평가 시점에 실패한다. - 모든 런타임에 존재하지 않는 프레임워크 훅(예: 서버 컴포넌트에서 undefined가 되는 ref 훅)은 데드 코드 제거를 위해 반드시 빌드 타임 서버 상수 뒤에서 게이트해야 한다. 런타임의 `typeof hook === 'function'` 검사로 보호해서는 안 된다. 이는 조건부 훅 호출이 되어버린다. - 출력을 생성하는 정식 경로는 하나만 유지한다. 출력을 변경할 때는 별도의 병렬 경로를 추가하는 대신 출력 생성 및 파서/컴파일러 테스트 스위트를 확장한다. ## 빌드 타임 상수로 코드를 걷어내고, 엔트리 포인트 매핑을 테스트로 고정하기 번들러가 빌드 타임에 제거할 수 있는 것과 런타임에만 존재하는 것을 구분하고, 진입점 매핑은 신중하게 설계한다. - 빌드 타임 상수(서버 빌드, 네이티브 빌드, 네이티브-웹 변형, 개발 모드)는 리터럴 값으로 치환되어, 번들러가 사용되지 않는 분기를 데드 코드로 제거할 수 있게 한다. dev 전용으로 이어지는 가장 바깥쪽 영역은 하나의 dev 가드로 감싸는 것을 우선한다. 작업을 건너뛰는 것을 warn 헬퍼 하나에만 의존하지 말고, 메시지 문자열이나 경고 인자를 dev 가드 바깥에서 구성하지 않는다. - `typeof window !== 'undefined'`와 같은 런타임 검사는 트리 셰이킹으로 제거할 수 없다. 코드를 번들에서 물리적으로 제거해야 한다면 런타임 검사가 아니라 빌드 타임 상수를 사용한다. - 진입점/서브패스 매핑은 소리 없이 문제가 생기는 흔한 원인이다. 플랫폼 확장자 기반 해석(네이티브 번들러가 플랫폼별 파일을 선택하도록 두는 방식)은, 일부 네이티브 번들러가 모든 플랫폼에 적용해버려 웹 브리지를 네이티브로 조용히 라우팅할 수 있는 `browser` 객체 맵보다 안전하다. `.js` 폴백은 안전한 크로스플랫폼 브리지여야 하며, 절대 플랫폼 전용 엔진이 되어서는 안 된다. 이러한 해석 결과는 resolution/tree-shake 테스트로 고정한다. - 조건부 `exports`가 복합(composite) 프로젝트에서 타입 해석을 깨뜨리는 경우, `browser` 필드를 우선 사용한다. 공개되는 모든 서브패스 디렉터리는 패키지의 `files` 목록에 추가한다. ## 실패하는 테스트를 먼저 쓰고, 최적화는 실제 작업량으로 측정하기 - 저장소 전체에서 엄격한 테스트 주도 개발(TDD)을 강제한다. 프로덕션 코드를 변경하기 전에, 원하는 계약을 담아 실패하는 테스트를 추가하거나 확장한 뒤, 그것을 통과시키는 데 필요한 만큼만 구현한다. 그 변경 없이는 실패했을 테스트 없이 사용자에게 보이는 동작을 배포하지 않는다. 기능이 아닌 작업에도 동일한 red-green 원칙을 따른다. - 최적화는 항상 인위적인 최선의 케이스가 아니라 현실적인 워크로드를 대상으로 마이크로벤치마크한다. 단독으로는 유리해 보여도 실제 호출자가 거치는 경로를 악화시키는 변경은 되돌린다. - 낮은 메모리 압박과 모노모픽(monomorphic) 함수를 목표로 최적화한다. - 사양에 근거한 동작은 요약이나 참고 사이트, 기억이 아니라 권위 있는 사양의 현재 편집자 초안(editor draft)에 근거를 둔다. 세션마다 관련 섹션을 다시 가져오고, 이를 고정하는 테스트 위에 주석으로 규범적 조항(normative clause)을 그대로 인용하며, 명확하게 이름 붙인 spec-compliance 블록 안에 규칙당 테스트 하나씩을 작성한다. 사양이 침묵하거나 모호한 경우, 테스트 옆에 그 편차를 문서화하고 추적 이슈를 링크한다. 이는 사양과 관련된 모든 변경에 적용된다. ## 사양의 규칙마다 테스트를 먼저 쓴 뒤 네이티브 폴리필 만들기 네이티브 런타임의 스타일 엔진이 어떤 CSS 기능을 이해하지 못할 때는 폴리필로 대응하되, 내부 구조를 리팩터링하기 전에 그 동작을 사양에 고정한다. 새로운 폴리필 또는 기존 폴리필을 재검토할 때의 절차(사양에 근거한 검증 블록에도 동일하게 적용): 1. 해당 세션에서 사용할 현재 편집자 초안을 가져온다. 조항 번호와 문구는 계속 바뀌므로, 오래된 발췌본을 권위 있는 것으로 취급하지 않는다. 2. 사양의 각 규칙을 정확히 테스트 하나로 옮기고, 그 위에 사양 원문을 그대로 인용하며, 관련 스위트 안의 이름 붙인 spec-compliance 블록 안에 작성한다. 3. 테스트를 먼저 작성하고, 현재 어떤 테스트가 실패하는지 기록한다. 4. 테스트가 통과할 때까지 구현한다. 통과하는 테스트를 구현에 맞춰 다시 손보지 않는다. 사양이 진실의 원천(source of truth)이다. 5. 회귀를 확인하기 위해 네이티브와 메인 스위트 전체를 실행한다. spec 블록이 통과하면, 내부 구현(토큰화, AST, 평가기)은 자유롭게 리팩터링해도 된다. 이 블록이 사용자가 관찰할 수 있는 동작을 고정해 주기 때문이다. - 런타임 버전이 배포되거나, 지원 범위가 바뀌거나, 폴리필이 추가·제거·변경될 때마다 사용자 대상 네이티브 호환성 대장(ledger)을 최신 상태로 유지한다. - 네이티브에서 실현 가능한 우회 방법이 없는 기능은, 문제가 되는 구문과 실행할 수 없는 이유, 구체적인 대안을 명시하는 안정적인 코드로 중복 제거된 경고를 발생시킨 뒤, 말이 안 되는 결과를 내보내는 대신 선언을 폐기(null로 처리)해야 한다. - 변형 간 동등성(parity): 브라우저가 이미 어떤 기능을 지원하는 경우, 원본 CSS를 그대로 전달한다. 브라우저가 아직 지원하지 않는 속성은 절대 내보내지 않는다. 네이티브 전용 prop 승격과 enum 확장은 네이티브 전용 빌드 상수 뒤에서 게이트한다. 각 폴리필 spec 블록에는 웹 변형을 켜고 끄면서 그 가시적 출력을 고정하는 parity 서브 스위트를 둔다. 그리고 특정 변형에 적용되지 않는 규범적 규칙은 (편차 주석과 동일한 엄격함으로) 명시적으로 제외 처리한다. ## 릴리스 노트에는 사용자에게 보이는 변화만 쓰고, 문서는 알맞은 곳에 두기 모든 산출물의 어조와 내용은 그것을 읽을 대상에 맞춘다. - 사용자를 대상으로 하는 글(릴리스 노트, 체인지로그 항목, PR 설명, 공개 문서)은 소스를 읽지 않은 사람도 이해할 수 있는 말로, 사용자가 관찰할 수 있는 동작만을 기술한다. 무엇이 지원되고 무엇이 지원되지 않는지, 그리고 이번 변경 자체의 경계를 명시한다. 관련 없는 미지원 항목까지 나열하지 말고 호환성 매트릭스를 링크한다. 내부 API 이름, 메커니즘, 코드 경로는 넣지 않는다. 이런 내용은 커밋 본문과 기여자 가이드에 둔다. - 동작은 현재 상태 그대로 기술한다. 아직 릴리스된 적 없는 상태에 대해서는 "이전에는", "더 이상 ~하지 않는다", "이제는", "대체한다" 같은 경과 서술을 피한다. 아직 릴리스되지 않은 기능에 대한 수정 사항은 그 기능 자체의 릴리스 노트에 합쳐 넣는다. - 릴리스 노트는 사용자에게 보이는 변경(버그 수정, 신규 기능, 호환성이 깨지는 변경)에 대해서만 작성한다. 내부 리팩터링, 툴링, 테스트, 그리고 같은 미출시 버전 안에서 도입된 코드에 대한 수정은 제외한다. - 자동 생성된 파일(체인지로그, 생성된 오류 맵)은 직접 편집하지 않는다. 해당 도구를 통해 다시 생성한다. - 기여자 대상 문서에는 특정 AI 제공사의 이름을 넣지 않는다. "AI 코딩 어시스턴트" 같은 중립적인 표현을 사용한다. - PR 본문, 이슈 본문, 댓글의 산문은 강제 줄바꿈(hard wrap)을 하지 않는다. 한 줄짜리 문단은 화면 크기에 맞춰 자동으로 재배치되지만, 강제 줄바꿈은 모바일이나 인용 뷰에서 깨져 보인다. 커밋 메시지 본문은 72컬럼 관례를 유지해도 된다. ### 문서 배치 - 기여자 가이드에는 소스 주석에 자연스럽게 자리 잡기 어려운 범용 규칙, 불변 조건, 경험을 통해 얻은 지식을 담는다. 잘 서술된 메커니즘 설명은 여기에 두지 않는다. - 독립된 지식 문서는 함정과 경험적 지식을 담는 것이지, 기능 메커니즘을 단계별로 설명하는 곳이 아니다. 메커니즘에 대해서는 소스 코드가 권위를 갖는다. - 소스 수준의 "왜"는 코드 옆의 주석/JSDoc에 둔다. 프로덕션 소스나 경고 메시지에는 사양 인용을 다시 적지 않는다. 앵커, 규범적 인용, 근거는 이를 고정하는 테스트에 둔다. 경고 메시지는 문제가 되는 구문과 구체적인 대안만 명시하고, 그 외에는 아무것도 담지 않는다. ## 측정한 성능 패턴을 쓰고, 실제 코드가 도는 데모 만들기 주요 JavaScript 엔진에서 마이크로벤치마크로 검증된 패턴(어디까지나 출발점이 되는 가설로 취급하고, 실제 맥락에서 다시 측정할 것): - 문자열 `+=` 결합은 모든 규모에서 `array.push()` + `join()`보다 빠르다. - `{...props, extra}` 스프레드가 `Object.assign`이나 `for..in` 복사보다 빠르다. - `for..in`이 `Object.keys()`에 루프를 더한 것보다 빠르다. - 컴파일된 정규식은 맵에 캐시한다. 작업 자체를 건너뛰기 위한 `indexOf` 사전 검사가 더 큰 이득을 준다. - 촘촘한 루프에서는 수동 `+` 결합이 템플릿 리터럴보다 빠르다. - (프레임워크의 엘리먼트 태그를 사용해) 원시 엘리먼트 객체를 직접 구성하는 것이 프레임워크의 엘리먼트 팩토리보다 훨씬 빠르다. - `new Array(n)`은 타입 피드백을 오염시키는 구멍 뚫린(holey) 배열을 만든다. 자주 쓰이는(hot) 배열은 미리 크기를 지정하지 않는다. 엔진 및 언어 관련 함정: - const에 대입되는 익명 클래스 표현식에는 `private` 수식자를 사용할 수 없다. - 타입 전용 스타(star) 임포트는 타입 시스템에 의해 제거되더라도, 번들러의 모듈 해석은 여전히 촉발할 수 있다. 데모 및 시각적 QA의 무결성(동작을 증명한다고 주장하는 모든 쇼케이스에 적용): - 가짜 데모는 절대 허용하지 않는다. 모든 위젯은 자신이 보여준다고 주장하는 실제 런타임 경로를 실제로 실행해야 한다. 어떤 기능이 특정 플랫폼에서 실행될 수 없다면, 그 셀은 (레이아웃 깨짐, 경고, 글리프 누락 등으로) 눈에 띄게 실패하도록 둔다. 그 실패 자체가 데모다. 사양을 흉내 내는 목(mock)으로 대체해서는 절대 안 되며, 실제 경로가 작동하는 것을 대신해 통과/실패 색상을 하드코딩해서도 안 된다. - 쇼케이스에서는 플랫폼 분기가 없는 옴니플랫폼(omniplatform) 컴포넌트를 사용한다. 플랫폼 편차에 대한 우회 처리는 앱이 아니라 라이브러리에 있어야 한다. - 정상 동작과 깨진 동작을 (레이아웃 이동, 색상 범람, 있고 없음 같은) 큰 차이를 통해 멀리서도 한눈에 구분할 수 있게 만들고, 모든 위젯이 라이트 모드와 다크 모드 양쪽에서 올바르게 보이도록 한다.