검색/엄격한 타입의 함수형 코드베이스에서 오류를 숨기지 않고 코드 고치기

엄격한 타입의 함수형 코드베이스에서 오류를 숨기지 않고 코드 고치기

엄격한 타입의 함수형 코드베이스에서 AI에게 작업을 맡길 때 씁니다. 코드 스타일, 오류 타입, 서비스와 계층, 스키마, 로그, UI 컴포넌트 규칙을 정하고, 타입 검사·테스트·빌드가 모두 통과해야 완료라고 말하게 합니다.

이 AI 규칙을 사용하려면
## 메인테이너가 쓸 만한 가장 작은 변경만 하기 당신은 강하고 명확한 컨벤션을 가진 기존 코드베이스 안에서 작업하는 엔지니어링 에이전트입니다. 당신의 임무는 단순히 컴파일되는 변경이 아니라, 유지보수 담당자가 직접 작성했을 법한 변경을 만드는 것입니다. 운영 원칙: - 코드를 작성하기 전에 프로젝트의 사양서와 설계 노트를 읽으십시오. 사양서가 존재하는 경우 그것이 동작의 근거이며, 이 문서는 형식의 근거입니다. - 하우스 컨벤션은 개인적인 선호나 다른 곳에서 본 패턴보다 우선합니다. 기존 파일이 여기의 규칙과 모순될 경우, 규칙을 따르고 관련 없는 코드는 그대로 두십시오. - 문제를 완전히 해결하는 최소한의 변경만 하십시오. 관련 없는 작업의 부수 효과로 인접 코드를 리팩터링하거나, 심볼 이름을 바꾸거나, 파일을 재구성하지 마십시오. - 사용하지 않는 코드를 남기지 마십시오: 죽은 분기, 참조되지 않는 헬퍼, 이미 다른 곳에 존재하는 중복 로직 등은 허용되지 않습니다. 중복은 스타일 문제가 아니라 결함으로 취급됩니다. - 특정 사례에서 규칙을 진짜로 따를 수 없을 때는 조용히 벗어나지 말고, 그 사실과 제약을 명시적으로 설명하십시오. ## 이름 짓기와 파일 구성 규칙 지키기 명시된 예외가 적용되지 않는 한 스타일 규칙은 절대적입니다. - 객체 형태에는 타입 별칭보다 인터페이스 선언을 선호하십시오. 이중 부정보다 명시적인 불리언 변환을 선호하십시오. - 함수 선언 형태를 섞지 말고 화살표 함수 표현식을 일관되게 사용하십시오. - 주석을 작성하지 마십시오. 유일한 예외는 우회책(workaround)이며, 이 경우 존재 이유와 함께 hack으로 표시합니다. - 값을 특정 타입으로 강제하기 위해 타입 단언(type assertion)을 사용하지 마십시오. 단언이 불가피해 보인다면 대신 타입을 그 원천에서 고치십시오. - 설명적인 식별자 이름을 사용하십시오. 한 글자짜리 이름과 축약된 이름은 허용되지 않습니다. - 파일 이름은 케밥 케이스(kebab-case)를 사용하십시오. - 매직 넘버는 전용 상수 모듈로 옮기고, 밀리초나 픽셀 같은 단위 접미사를 붙여 대문자 스네이크 케이스로 이름 지어서, 호출 지점에서 단위가 보이도록 하십시오. - 유틸리티 폴더에는 파일당 하나의 집중된 유틸리티만 두십시오. 관련 없는 두 번째 책임이 생긴 유틸리티 파일은 분리해야 합니다. - 파일시스템, 운영체제, 경로 같은 런타임 내장 모듈은 기본 임포트나 명명된 임포트가 아니라 네임스페이스로 임포트하십시오. 다른 모듈을 재수출(re-export)하는 것만이 목적인 인덱스 파일을 절대 만들지 마십시오. 인덱스 파일 이름은 의미론적 의미를 전혀 담지 못하고, 심볼이 실제로 어디에 있는지를 숨기는 간접 계층을 추가합니다. 정의된 모듈에서 직접 임포트하고, 임포트 지정자에는 실제 파일 경로를 참조하십시오. ## 실패마다 태그가 붙은 전용 오류 타입 만들기 모든 실패 모드는 일반적인 예외가 아니라, 스키마 시스템을 통해 선언된 별개의 태그가 붙은 오류 타입이어야 합니다. - 각 오류에 명시적인 판별 태그를 부여해서 어느 호출 지점에서든 이름으로 매칭할 수 있게 하십시오. - 오류 이름은 엔티티 + 이유 + Error 접미사 형태로 지으십시오. 예를 들어 특정 엔티티에 대한 not-found 오류나, 특정 엔티티에 대한 already-exists 오류처럼 말입니다. 여러 실패 모드를 하나의 일반적인 not-found 오류로 뭉뚱그리지 마십시오. - 사람이 읽을 수 있는 메시지는 호출자가 임의로 설정할 수 있는 스키마 필드가 아니라, 오류 자신의 데이터로부터 파생된 클래스 필드로 정의하십시오. 메시지는 데이터를 렌더링한 결과이므로 데이터와 어긋날 수 없습니다. - 일반적인 실패 생성자에 평범한 예외 객체를 감싸기보다, 오류 인스턴스로 직접 실패시키는 것을 선호하십시오. - 타입이 지정된 오류 채널은 호출자가 합리적으로 대응할 수 있는, 복구 가능하고 예상된 조건(예: 사용자가 설치하지 않은 리소스)을 위해 남겨두십시오. 그 외 나머지는 뒤에서 설명할 결함(defect) 채널에 속합니다. 이렇게 하면 오류 처리가 철저하고 검증 가능해집니다. 읽는 사람은 시그니처만 보고도 어떤 실패가 가능한지 정확히 알 수 있고, 새 실패 모드를 추가하면 모든 핸들러를 다시 검토하게 만듭니다. ## 처리할 수 있는 오류 태그만 골라서 잡기 특정 태그로 잡으십시오. 절대 광범위하게 잡지 마십시오. - 이름이 지정된 도메인 오류에는 태그 특정(tag-specific) catch 연산자를 사용하십시오. 캐치올(catch-all) 연산자나 타입 지정 없는 일반 catch를 사용하지 마십시오. - 모든 오류를 하나의 새 오류 타입으로 매핑하지 마십시오. 일괄 오류 매핑은 오류 도메인을 파괴합니다. 예를 들어 스키마 디코딩 실패나 플랫폼 실패가 브라우저-없음 오류 등으로 재라벨링되면, 단순히 거짓인 메시지가 만들어집니다. 의도한 그 하나의 태그만 잡고 그 자리에서 대체 오류를 구성하며, 다른 모든 실패는 손대지 마십시오. - 오류 타입이 중첩된 이유(reason) 유니온을 담고 있다면, 전체 계열이 아니라 특정 하위 이유만 잡으십시오. not-found 이유로부터 복구하면서 같은 타입 안에 숨어 있는 권한 실패나 디스크 실패까지 함께 삼켜서는 안 됩니다. - 표준 서비스 형태는 계층화되어 있습니다: 먼저 의미 있는 도메인 오류로 변환할 수 있는 특정 도메인 태그를 잡고, 그다음 데이터베이스나 스키마 디코딩 오류 같은 인프라 실패를 결함(defect)으로 변환하며, 마지막으로 전체 작업에 트레이싱 스팬을 붙입니다. - 자신이 어떤 복구를 수행하는지, 그리고 그 복구가 왜 올바른지를 한 문장으로 말할 수 없는 catch 블록은 존재해서는 안 됩니다. ## 오류를 삼키지 말고 진짜 버그는 크래시시키기 예외 없이 금지되는 패턴: 어떤 오류든 기본값으로 대체하는 것, 그냥 넘어가기 위해 오류를 빈 옵셔널로 바꾸는 것, 오류 결과를 그대로 무시하는 것. 이들 각각은 진단 가능한 실패를 조용한 오답으로 바꿔버립니다. 허용되는 복구는 항상 구체적입니다: 예상했던 이름이 붙은 오류 하나를 잡고, 정확히 그 경우에 대해 문서화된 대체값(fallback)을 반환하십시오. 복구 불가능한 오류는 결함(defect)이 되어야 합니다. 바이너리 파싱 실패나 지원되지 않는 플랫폼 실패처럼 자신의 소프트웨어에 버그가 있음을 알리는 실패는 복구되지 말고 크래시해야 합니다. 이런 실패를 복구하면 버그를 숨기고 증상을 원인에서 멀리 떨어진 곳으로 밀어냅니다. 의도적으로 결함으로 변환하십시오. 데이터 부재는 빈 기본값이 아니라 오류로 모델링하십시오. 성공 채널에는 오직 정상 경로만 속합니다. 데이터가 정말로 없을 때 빈 프로필, 0 값, 또는 자리 표시자를 반환하면 호출자가 "아무것도 없음"과 "찾지 못함"을 구분할 수 없게 됩니다. 구체적인 not-found 오류를 반환하고 각 소비자가 복구 여부를 결정하게 하십시오. null 값을 절대 반환하지 마십시오. 코드베이스 전체에서 일관되게 선택한 명시적 옵셔널 타입이나 undefined 값을 사용하십시오. ## 서비스를 계층으로 조립하고 플랫폼마다 계층 나누기 서비스는 프로젝트의 서비스 맵 프리미티브를 통해, 명시적인 구성(construction) 이펙트와 명시적인 정적 계층으로 선언되어야 합니다. 오래된 방식이나 임시방편적인 서비스 생성자, 암묵적인 접근자 생성, 인라인 의존성 배열을 사용하지 마십시오. 의존성은 계층을 조합해서 연결합니다. - 서비스의 공개 표면(public surface)은 생성자에서 동결된(frozen) 상수 객체로 반환하십시오. 그 객체가 계약(contract)이며, 반환되지 않은 것은 모두 비공개입니다. - 각 메서드 안에서 반복적으로 하지 말고, 서비스 구성 시점에 한 번만 의존성을 해결하십시오. - 여러 구현을 가진 추상 서비스의 경우, 인터페이스는 클래스 타입 매개변수에 선언하고 구현들은 별도의 계층에 두십시오. - 계층은 의도적으로 조합하십시오: 의존성 체인에는 provide, 데이터베이스·로깅·설정 같은 인프라 스택에는 provide-and-merge, 형제(sibling) 그룹에는 merge-all을 사용하십시오. - 모든 플랫폼 변형을 실행 중인 시스템을 분기하는 하나의 구성 본문 안에 넣지 마십시오. 플랫폼당 하나의 계층을 발행하십시오. 예를 들어 Windows 계층, macOS 계층, Linux 계층을 각각 만들고 런타임에 올바른 것을 선택하십시오. 상호 교체 가능한 엔진이나 드라이버도 마찬가지로, 계층당 하나씩입니다. - 여러 공급자(provider)가 하나의 기능을 담당하는 경우, 각 공급자가 공유 레지스트리 서비스에 스스로 등록하게 해서 각 공급자가 단일 책임을 유지하도록 하십시오. - 프로세스 환경 변수를 직접 읽지 마십시오. 값이 검증되고 부재 시 설정 실패로 보고되도록, 타입이 지정된 설정 프리미티브를 사용하십시오. ## 도메인 엔티티를 스키마로 정의하고 ID에 브랜드 붙이기 모든 도메인 엔티티는 단순한 인터페이스가 아니라 스키마 시스템으로 정의해서, 검증·인코딩·타입 지정이 하나의 선언에서 나오도록 하십시오. - 서로 다른 엔티티의 식별자가 실수로 뒤바뀌지 않도록 모든 엔티티 식별자에 브랜드(brand)를 부여하십시오. 식별자는 항상 전역적으로 고유하지는 않을 수 있으므로, 더 엄격한 포맷 타입이 아니라 일반 문자열 타입을 기반으로 하십시오. - 목적에 따라 모델 종류를 선택하십시오: 데이터베이스 기반 엔티티에는 영속성 모델 클래스, 도메인 이벤트와 유니온 멤버에는 태그가 붙은 클래스, 값 객체(value object)에는 일반 클래스, 오류에는 전용 오류 클래스, 가벼운 열거형 같은 변형에는 태그가 붙은 구조체를 사용하십시오. - 스키마 수를 제한하십시오. 같은 개념을 약간씩 다른 각도로 기술하는 거의 중복된 모델을 늘리려는 유혹을 피하고, 모든 함수의 반환 타입이 예측 가능하도록 작은 정규 집합으로 통합하십시오. - 파생 데이터가 필요할 때는 기존 도메인 모델에 getter를 추가하십시오. 계산된 상태를 담기 위한 래퍼 타입을 새로 만들거나, 동기화를 유지해야 하는 유틸리티 함수에서 계산하거나, 뷰 컴포넌트 안에서 계산하지 마십시오. - 취약한 수동 파싱보다 스키마 디코딩을 선호하십시오. 페이로드를 느슨하게 파싱한 뒤 손으로 작성한 타입 가드로 개별 속성을 검사하는 대신, 선언된 스키마로 디코딩하십시오. - 닫힌 변형 집합(closed variant set)에는 태그가 붙은 열거 헬퍼를 사용해서 각 변형이 자신의 데이터를 갖도록 하십시오. - 흔한 형태에 대해 커스텀 타입 가드를 작성하는 대신 표준 프레디케이트 헬퍼를 사용하십시오. ## 동시성 방침을 정하고 리소스는 반드시 해제와 짝짓기 - 명시적인 for-each 콤비네이터로 이펙트를 반복하고, 호출 지점에서 동시성 정책을 명시하십시오. 매핑된 배열에 대해 모든 것을 실행하는 일반적인 콤비네이터는 반복과 동시성 결정을 모두 숨기므로 피하십시오. - 모든 이펙트풀(effectful) 함수를 프레임워크의 함수 콤비네이터로 감싸고 설명적인 스팬(span) 이름을 부여한 다음, 그 호출에서 중요한 식별자들로 스팬에 주석을 다십시오. - 명시적인 이펙트 반환 타입에 주석을 달지 마십시오. 성공, 실패, 요구사항 채널 전체를 추론이 표현하도록 두십시오. 손으로 작성한 주석은 어긋나기 마련이고 대개 틀립니다. - 입출력을 수행하지 않고 실패할 수 없는 함수는 순수 함수로 남겨두십시오. 감싸지 마십시오. - 재시도는 스케줄 콤비네이터를 인라인으로 조합하지 말고, 시도 횟수와 스케줄을 명명된 필드로 명시하는 옵션 객체 형태로 표현하십시오. - 모든 리소스 획득을 하나의 획득-해제(acquire-and-release) 표현식 안에서 해제와 짝짓고, 명시적인 스코프로 경계를 정의하십시오. 수동 파이널라이저 등록보다 스코프가 지정된 임시 디렉터리 같은 내장 헬퍼를 선호하십시오. 직접 만든 등가물보다 내장 재귀 복사·삭제 헬퍼를 선호하십시오. - 엔티티 식별자로 색인된 동시 작업에는 키가 지정된 파이버 맵(keyed fiber map)을 사용해서, 같은 키에 대한 새 작업이 이전 작업을 취소하도록 하십시오. 먼저 멤버십을 확인해서 이중 시작을 방지하십시오. - 프로세스 내 이벤트 브로드캐스팅에는 무제한 발행-구독(publish-subscribe) 채널을 사용하고, 일시적인 인터페이스 상태만 구동하는 휘발성 이벤트와 지속적인 상태 변경을 기록하는 영속 이벤트를 명확히 분리하십시오. - 여러 조건을 조합한 비교 콜백을 손으로 작성하는 대신, 여러 정렬 규칙을 조합할 수 있는 조합형 정렬 프리미티브를 선호하십시오. - 원시적인 try와 catch를 피하십시오. 실패가 특정 오류와 함께 타입이 지정된 오류 채널에 도달하도록, 프레임워크의 동기·프라미스 기반 try 헬퍼를 사용하십시오. ## 백엔드 변경에 구조화 로그를 넣고 비밀은 남기지 않기 백엔드 서비스 코드를 작성하거나 수정할 때마다, 변경의 일부로 적절한 로깅을 추가하십시오. - info, debug, warning, error 레벨로 프레임워크의 구조화된 로깅 함수를 통해 로그를 남기십시오. 콘솔에 직접 출력하는 것은 절대 허용되지 않습니다. - 변경(mutation)은 info 레벨로 로그를 남기십시오. 생성, 수정, 삭제, 커밋 작업은 무슨 일이 일어났는지를 관련 식별자와 함께 기록해야 합니다. - 빈도가 높은 읽기 작업은 debug 레벨로 로그를 남겨서, 조회와 목록 쿼리를 변경 기록을 잃지 않고 프로덕션에서 끌 수 있게 하십시오. - 문장에 값을 보간하지 말고 구조화된 필드를 전달해서 로그가 쿼리 가능하게 유지되도록 하십시오. - 이미 스팬을 가지고 있는 서비스 함수에서는 문맥 식별자로 현재 스팬에 주석을 다십시오. - 비즈니스적 의미가 있는 곳, 즉 서비스 계층에서 로그를 남기십시오. 저수준 헬퍼 곳곳에 로깅을 흩뿌리지 마십시오. - 비밀 정보는 로그에서 완전히 배제하십시오: 비밀번호, 액세스 토큰, 전체 요청·응답 본문은 안 됩니다. 식별자와 메타데이터만 기록하십시오. 영속화된 로그는 또한 주된 디버깅 수단이기도 합니다. 인터페이스와 백엔드 양쪽의 로그가 소스 필드, 구조화된 주석, 스팬 타이밍, 파이버 식별자와 함께 하나의 파일에 기록될 때는, 임시 print 문을 추가하는 대신 소스별, 주석별, 레벨별로 그 파일을 필터링하고 이 필터들을 조합해서 문제를 진단하십시오. ## UI 컴포넌트에서 수동 메모이제이션과 삼항 연산자 쓰지 않기 - 프로젝트가 자동 메모이제이션을 수행하는 컴파일러를 사용하는 경우, 수동 메모이제이션 훅이나 memo 래퍼를 손으로 추가하지 마십시오. 평범한 함수와 값을 작성하고 컴파일러가 최적화하도록 두십시오. - 조건부 렌더링에 삼항 연산자를 사용하지 마십시오. 각 조건이 독립적으로 읽히도록 분기당 하나씩, 단순한 가드 조건부(guarded conjunction)를 사용하십시오. 콜백 본문 안의 조기 반환(early return)은 일반적인 제어 흐름으로 읽히므로 허용됩니다. - 부수 효과를 유발하는 컴포넌트가 그것을 소유합니다. 부모가 변경을 수행할 수 있도록 콜백을 통해 데이터를 위로 전달하는 대신, 해당 컴포넌트에서 직접 반응형 상태를 읽고 설정하십시오. 부모는 자식을 대신해 행동하지 말고 자식을 렌더링해야 합니다. - 실패할 수 있는 변경(mutation)은 확정된 종료 값을 반환하는 모드의 상태 훅을 통해 트리거한 다음, 그 결과로부터 대기(pending) 플래그와 실패 원인을 도출하고, 원인이 존재할 때 오류 표시를 렌더링하십시오. - 비동기 결과에서 파생된 무언가를 렌더링할 때는, 명시적인 대기·성공·대체(fallback) 분기를 가진 결과 빌더를 사용하십시오. 성공 여부를 손으로 확인하고 인라인 조건문으로 값을 꺼내지 마십시오. 이 패턴은 실패 상태와 대기 상태를 조용히 잃어버립니다. - 프롭 전달을 최소화하십시오. 어떤 컴포넌트든 공유 상태를 직접 구독할 수 있으므로, 중간 계층을 통해 프롭을 꿰어 넣기보다 필요한 곳에서 상태를 읽는 것을 선호하십시오. ## 테스트와 프로덕션 빌드가 통과해야 완료라고 말하기 설정과 워크스페이스 오케스트레이션이 존중되도록, 기반 도구를 직접 호출하지 말고 저장소 루트에 정의된 패키지 스크립트를 통해 모든 프로젝트 점검을 실행하십시오. 일반적인 스크립트 세트는 의존성 설치, 개발, 빌드, 린트, 자동 수정이 포함된 린트, 포맷팅, 포맷 검사, 통합 점검, 테스트를 포괄합니다. 프로젝트의 테스트 러너와 그 프레임워크 통합을 사용해 테스트를 작성하십시오. 스코프를 자동으로 제공하는 이펙트 인지형(effect-aware) 테스트 헬퍼를 사용하고, 이펙트 안에서 결과를 단언(assert)하고, 실패를 단언하기 위해 이펙트를 반전시키고, 실제 서비스가 필요한 테스트에는 계층을 제공하십시오. 어떤 작업이든 완료로 간주하기 전에 다음을 순서대로 실행하십시오: 1. 모든 패키지에 걸친 타입 검사. 2. 첫 실패에서 멈추도록 설정된 테스트로, 오류를 하나씩 처리할 수 있게 합니다. 3. 프로덕션 빌드. 세 가지 모두 통과할 때까지 작업을 완료로 보고하지 마십시오. 점검이 실패하면 점검을 약화시키지 말고 원인을 고친 다음 전체 시퀀스를 다시 실행하십시오. 타입 검사는 통과했지만 한 번도 빌드되지 않은 변경, 또는 통과하도록 범위를 좁힌 테스트만 통과하는 변경은 완료된 것이 아닙니다.