하네스 엔지니어링, 결국 두 개의 질문이었다

AI에게 일을 맡기는 환경 만들기 — 무엇을 규칙으로 둘지, 어떤 등급을 줄지

AIReact

루프팩 1주차 과제로 AI 협업 환경을 처음부터 세팅했다. 실무에선 기존 설정 위에 규칙 하나 얹어 본 게 전부였는데, 이번엔 빈 프로젝트에서 시작하니 규칙 하나하나의 의미를 다시 생각하게 됐다. is-it-going-well

왜 환경부터 만들어야 할까

예전엔 내가 프로젝트 컨벤션을 머리에 넣고 코드를 짰다. 이제는 구현을 에이전트가 한다. 그러니 컨벤션은 내 머리가 아니라 **에이전트의 메모리(CLAUDE.md)**에 들어가야 한다.

이게 없으면 같은 요구사항을 줘도 매번 다른 결과물이 나온다. 내가 생각하는 "좋은 코드"에 도달하기까지 시도가 반복되고, 그 비용만큼 설계에 쓸 시간이 줄어든다. 구현을 믿고 맡기려면 맡길 만한 환경부터 있어야 한다.

규칙을 많이 적을수록 좋을까? — 아니다

Claude 공식 문서는 오히려 반대로 권한다: 파일당 200줄 이하, 그룹화, 검증 가능할 만큼 구체적, 모순 없게.

문제는 이걸 글자 그대로 지키기가 어렵다는 점이다. 컴포넌트 규칙을 빠짐없이 적으려다 보면 Hooks 규칙·미사용 변수·import type 분리·포맷팅까지 줄줄이 적게 되는데, 이건 전부 ESLint·TypeScript가 이미 잡는 것들이다. 같은 규칙을 설정 파일과 CLAUDE.md 두 곳에 적으면 200줄은 금방 넘고, 한쪽만 고치면 두 규칙이 어긋난다.

그래서 CLAUDE.md 상단에 "설정 파일이 단일 출처"라고 박아 뒀다. 도구가 잡는 항목은 문서에 다시 적지 않는다.

### 기반 규칙

- 1단계 하네스인 정적 분석 도구(ESLint, TypeScript)가 강제하는 규칙을 기반 규칙으로 따른다.
  설정 파일(`eslint.config.js`·`tsconfig.app.json`·`.prettierrc.json`)이 단일 출처이며,
  도구가 잡아내는 항목은 여기서 중복 서술하지 않는다.
  - Hooks 규칙, 미사용 변수/파라미터, `import type` 분리, 포맷팅·스타일 등.
- 도구 경고(`warn` 포함)도 무시하지 않는다.

이 한 문단 덕분에 CLAUDE.md에는 도구로 강제할 수 없는 것 — 파생값의 useState 금지, useEffect는 외부 시스템 동기화 전용 같은 판단이 필요한 규칙만 남았다.

질문 1. 무엇을 규칙으로 둘까

원칙은 하나였다. 최소한으로 두고, 실제로 깨졌던 곳에만 추가한다. 추천 규칙을 전부 켜는 대신, 이전 React 작업에서 내가 실제로 당했던 지점만 떠올려 적용했다.

// eslint.config.js (rules 부분)
rules: {
  // 어기면 거의 확실히 깨짐 → error
  'react-hooks/rules-of-hooks': 'error',
  // 의도적 예외 여지 있음 → warn (stale closure는 직접 판단)
  'react-hooks/exhaustive-deps': 'warn',
  '@typescript-eslint/no-floating-promises': 'warn',
  // 타입 단언(as) 금지 — 좁히려면 가드/제네릭으로
  '@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }],
  // 의미없는 이름 금지 (data·temp·flag…)
  'id-denylist': ['warn', 'data', 'temp', 'tmp', 'flag', 'val', 'foo', 'bar', 'err'],
  // 핸들러 네이밍: onXXX는 props 전달용 예약, 선언 핸들러는 handleXXX
  'no-restricted-syntax': [
    'error',
    { selector: 'VariableDeclarator[id.name=/^on[A-Z]/]',
      message: '핸들러는 handleXXX로. onXXX는 props 전달용 예약입니다.' },
    { selector: 'FunctionDeclaration[id.name=/^on[A-Z]/]',
      message: '핸들러는 handleXXX로. onXXX는 props 전달용 예약입니다.' }
  ]
}

규칙마다 무엇을 바꾸는지를 한 줄로 붙여 두면, 나중에 "이거 왜 켰더라"를 다시 묻지 않게 된다.

질문 2. 어떤 등급을 줄까

규칙을 정했다면 다음 질문. 모든 규칙이 똑같이 중요하진 않다. ESLint의 off/warn/error를 이렇게 나눴다.

티어기준예시
off취향·스타일포맷팅 → Prettier에 위임
warn버그 방어하지만 의도적 예외 여지 있음exhaustive-deps, id-denylist
error어기면 확실히 깨지거나, 지키기 쉬움rules-of-hooks, as 금지, onXXX 금지

가장 고민한 건 exhaustive-deps였다. stressedcat 결론은 warn, 근거는 React 팀의 선택과 같다. 의존성 배열은 개발자가 의도적으로 비우는 경우가 있어서, error로 막으면 의도한 코드까지 차단된다. 버그는 방어하되 최종 판단은 사람에게 남기는 warn이 맞다. 단 warn이 "무시해도 된다"는 뜻은 아니다 — stale closure가 의심되면 직접 고쳐야 한다. 짝이 되는 rules-of-hooks는 어기면 거의 확실히 깨지므로 error다.

적어 두기만 하면 작동할까

규칙을 적는 것과 실제로 작동하는 건 다른 문제다. 그래서 CLAUDE.md 맨 아래에 작업 완료 전 통과해야 할 점검 목록을 박아, 하네스가 "선언"이 아니라 매 작업의 게이트로 동작하게 했다.

## 코드 리뷰 규칙

리뷰/작업 완료 전 점검:
- [ ] `pnpm lint`, `pnpm build` 통과
- [ ] 변경 범위가 요청 범위를 벗어나지 않음
- [ ] 새 의존성 무단 추가 없음
- [ ] console.log, 디버깅 코드 없음

솔직히 지금의 검증은 여기까지다. 규칙이 안티 패턴을 몇 번 걸러 냈는지 같은 수치는 아직 없다. 구현 과제가 시작되는 다음 주가 진짜 시험대다. pnpm lint가 핸들러 네이밍을 잡거나 exhaustive-deps 경고가 실제 버그로 이어지는 순간을, 다음 글에서 숫자와 함께 회고해 보려 한다.

마무리

거창해 보이지만 기초 단계에서 한 일은 두 질문을 반복한 것뿐이다.

  • 무엇을 규칙으로 둘까? → "이게 내가 실제로 깨졌던 곳인가?"
  • 어떤 등급을 줄까? → "어기면 확실히 깨지나, 의심만 가나?"

그리고 그 답을 머릿속이 아니라 eslint.config.js와 CLAUDE.md에 적어 도구가 강제하게 만들었다. 규칙을 기준 없이 들이붓는 건 쉽지만 노이즈가 끼거나 정작 필요한 걸 놓친다. 품이 들더라도 내 프로젝트에 맞는 규칙만 고르는 쪽을 택했다.


레퍼런스