사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
에러 처리
이미 결제된 주문은 수정할 수 없고, 사용자는 그 이유를 자신의 언어로 알 수 있어야 합니다. Akan에서는 서버가 dictionary key를 던지고, 클라이언트가 그 key를 번역해 보여줍니다.
에러 하나는 다음 네 단계를 거칩니다:
위치하는 일
order.dictionary.ts
1. 선언:
.error({})에 에러 문장을 [en, ko] 쌍으로 등록합니다.order.document.tsorder.service.ts
2. 던지기: 비즈니스 규칙이 깨지면
new Err("order.error.notDraft")를 던집니다.HTTP · WebSocket
3. 전달: 서버는 번역하지 않은 key와 상태 코드,
data를 응답으로 보냅니다.order.store.ts
4. 표시: fetch가 응답을
Err로 복원하고, store action이 번역해 toast로 띄웁니다.에러 선언하기
먼저 모듈의 dictionary에 선언합니다.
.error({})에 선언한 key만 Err에 넣을 수 있으므로, 오타는 TypeScript가 잡아 줍니다.아래 코드는 체인의 나머지 단계를 생략했습니다:
apps/myapp/lib/order/order.dictionary.ts
- key 경로는 정해져 있습니다.
orderdictionary의notDraft는order.error.notDraft로 던집니다. - 치환 자리.
{productName}은 에러와 함께 넘긴data값으로 채워집니다. 아래 'Data 사용하기'에서 봅니다. - 한국어 문장은 '다.'로 끝냅니다.
.error()의 한국어 문구는다.로 끝나는 완결된 문장으로 씁니다. - 성공 메시지는 에러가 아닙니다.
order.addItemSuccess같은 toast 문구는.translate({})에 선언합니다.
Err 던지기
사용자가 이해하고 고칠 수 있는 비즈니스 규칙이 깨지면
Err를 던집니다. 문서 자신의 상태에 대한 규칙은 document method에 두면 모든 service가 같은 검사를 공유합니다.초안 상태인 주문만 제목을 바꿀 수 있습니다:
apps/myapp/lib/order/order.document.ts
규칙을 두는 곳
두는 곳규칙
order.document.ts
초안 주문만 수정할 수 있다는 것처럼, 문서 자신의 상태에 대한 전제 조건입니다.
order.service.ts
담으려는 상품이 존재해야 한다는 것처럼, 다른 문서를 불러오거나 비교하는 규칙입니다.
order.signal.ts
주문한 본인만 부를 수 있다는 것처럼, 누가 endpoint를 부를 수 있는지를 guard로 정합니다.
Err를 가져오는 곳
파일가져오는 법
*.document.ts*.service.ts*.signal.ts
서버 파일은 모듈의
dict barrel에서 가져옵니다.*.tsx
UI 파일은 앱의 client 진입점에서, lib에서는
@libs/<lib>/client에서 가져옵니다.common/**env/**
Err를 import할 경로가 없으므로, 에러를 던지는 코드를 이 폴더에 두지 않습니다.


throw new Error는 쓰지 않습니다. no-throw-raw-error lint 규칙이 테스트, *.constant.ts, common/**, env/**를 뺀 apps/**·libs/** 전체에서 빌드를 실패시킵니다. 또 일반 Error는 사용자에게 뭉뚱그린 500으로만 전달됩니다.상태 코드 고르기
new Err()는 상태 코드 400으로 응답합니다. HTTP 의미가 중요할 때만 이름 있는 helper를 던지세요. helper는 모두 new Err()와 같은 인자를 받습니다.new Err(key)400
기본값으로, 비즈니스 규칙이 요청을 거절했다는 뜻입니다.
Err.BadRequest400
같은 400을 이름으로 드러냅니다.
Err.Unauthorized401
호출자가 로그인하지 않았거나 신원을 증명하지 못했습니다.
Err.Forbidden403
사용자는 확인됐지만 이 동작은 할 수 없습니다.
Err.NotFound404
요청한 레코드가 없습니다.
Err.Conflict409
현재 상태에서는 이 동작을 받을 수 없습니다.
service에서는 주문 상태와 상품 존재 여부에 서로 다른 상태 코드를 고릅니다:
apps/myapp/lib/order/order.service.ts
get은 에러를 던지고,load는 null을 돌려줍니다.getOrder(id)는 레코드가 없으면 일반 에러를 던지므로 호출자는 뭉뚱그린 500을 받습니다.- 없는 레코드는 직접 답합니다. 사용자가 없는 id에 닿을 수 있다면
loadProduct(id)를 부르고,null이면Err.NotFound를 던집니다. - HTTP 상태 코드도 같습니다. 응답이 같은 코드로 나가므로 프록시와 접근 로그에도 404나 409로 남습니다.
Data 사용하기
번역 문장에 값이 필요하면
data를 함께 넘깁니다. 서버는 dictionary key를 error에 그대로 두고, 치환할 값을 data로 옆에 보냅니다.Err의 두 번째 인자가 그 data 객체입니다:apps/myapp/lib/order/order.document.ts
- 이름이 치환 자리와 맞아야 합니다.
data.productName이 모든 언어 문장의{productName}자리를 채웁니다. - 문자열과 숫자만 씁니다. 에러 toast는 문자열과 숫자 값만 채우고, 나머지는 버립니다.
- 빠진 값은 그대로 보입니다. 값이 없는 자리는
{quantity}처럼 쓴 그대로 나오므로, 빠진 값이 내용이 아니라 버그로 드러납니다.
클라이언트 처리
fetch는 에러 응답을
Err로 복원하고, 모든 store action은 그것을 잡는 wrapper 안에서 실행됩니다. wrapper가 key를 사용자 언어로 번역해 toast로 띄우므로, action에는 성공 경로만 쓰고 try/catch는 쓰지 않습니다.store action은 endpoint를 부르고 성공한 경우만 처리합니다:
apps/myapp/lib/order/order.store.ts
버튼은 실패를 전혀 모릅니다. action을 호출하고 나머지는 wrapper에 맡깁니다:
apps/myapp/lib/order/Order.Util.tsx
- toast 뒤에 다시 던집니다. wrapper는 toast를 띄운 뒤 에러를 다시 던지므로, 실패하면
await st.do.addItemToOrder()뒤의 코드는 실행되지 않습니다. - 네트워크 실패도 같습니다. 타임아웃, 서버 연결 실패, 서버 재시작도
base.error.*key를 가진Err로 도착해 같은 toast로 보입니다. - 입력 검사는
msg.error로 합니다. 서버를 부르기 전에 클라이언트에서 검사할 때는msg.error("<key>")를 부르고 바로 return하며, throw하지 않습니다. try/finally는 spinner용입니다. UI 코드에서는 spinner를 되돌릴 때만 쓰고, 에러를 잡는 일은 wrapper에 맡깁니다.
응답 형태
HTTP와 websocket 에러는 거의 같은 필드를 가집니다. 직접 만들 일은 거의 없지만, 알아 두면 디버깅이 쉬워집니다.
앞의
stockNotEnough 에러는 이렇게 도착합니다:필드설명
error
번역된 문장이 아니라, 던진 dictionary key 그대로입니다.
statusCode
기본 400 또는 helper의 상태 코드이며, HTTP 응답의 상태 코드도 같습니다.
data
치환에 쓸 값으로, 넘겼을 때만 들어갑니다.
details
디버깅용 추가 정보로, 있을 때만 들어갑니다.
path
endpoint 경로로, HTTP 응답에만 있고 websocket 에러 프레임에는 없습니다.
timestamp
서버가 응답한 시각(ISO 문자열)입니다.
일반 Error가 새어 나가면
Err가 아닌 에러는 500으로 응답하므로, 사용자는 dictionary 문장을 보지 못합니다:던진 것호출자가 받는 것
ErrErr.*
자기
statusCode로 응답하고, error에는 dictionary key가 들어갑니다.ErrorgetOrder(missingId)
500으로 응답하며, 배포된 빌드에서는
error가 Internal Server Error입니다.- 개발 중에는 모두 보입니다.
akan start에서는 응답에 실제 메시지와 stack이 들어갑니다. - stack은 서버 로그에 있습니다. 응답이 가리더라도 이런 500은 모두 stack과 함께 로그에 남습니다.
- 배포된 빌드를 디버깅할 때.
AKAN_ERROR_DETAIL=1을 설정하면 응답에 실제 메시지가 다시 들어갑니다.
팁
- key는 도메인과 이유가 보이게 짓습니다. 예:
order.error.notDraft,order.error.stockNotEnough,user.error.wrongPassword. - 서버에서 번역하지 않습니다. key와
data만 보내고, 사용자 언어는 클라이언트가 고르게 둡니다. - 원격
Err는 그대로 넘깁니다. 서버 간fetch.x(…, { origin })호출은 원격Err를 그대로 복원하므로,new Error로 감싸거나 새 key로 바꾸지 말고 그대로 다시 던지세요.