nworks calendar list
치면 오늘 일정이 나온다.
nworks message send --channel C001 --text "배포 완료"
치면 팀 채널에 메시지가 간다.
NAVER WORKS를 터미널에서 쓸 수 있는 CLI를 만들었다. 이 글은 그 과정을 정리한 거다. 인증 구조에서 터진 것들, scope 의존성 지옥, int64 overflow, npm 배포 시 주의할 것들. 직접 CLI를 만들어보려는 사람한테 도움이 되면 좋겠다.
왜 만들었냐면, Google Workspace에는 gws라는 CLI가 있다. 캘린더 조회, 드라이브 업로드, 메일 전송을 터미널에서 한 줄로 끝내는 도구다. NAVER WORKS에는 이런 게 없었다. API 문서는 있는데 CLI가 없길래 직접 만들었다.
NAVER WORKS API 인증 구조: 두 갈래 길
NAVER WORKS API를 쓰려면 먼저 Developer Console(dev.worksmobile.com)에서 앱을 등록해야 한다. Client ID와 Client Secret을 받는다. 여기까지는 다른 OAuth 앱이랑 같다.
문제는 그 다음이다. NAVER WORKS는 인증 방식이 두 가지다.
Service Account (JWT) 는 봇 전용이다. Private Key로 JWT를 만들어서 토큰을 발급받는다. 메시지 전송, 채널 구성원 조회 같은 봇 동작에 쓴다. 사용자 인터랙션 없이 서버에서 자동으로 돌릴 수 있다.
User OAuth 는 사용자 본인 권한으로 동작한다. 브라우저에서 로그인하고, Authorization Code를 받아서 토큰을 발급받는다. 캘린더, 드라이브, 메일, 할 일, 게시판 — 사용자 데이터에 접근하려면 이쪽이다.
처음에는 Service Account 하나로 다 될 줄 알았다. 캘린더 API를 호출했더니 돌아온 게 이거다.
Error: SERVICE_ACCOUNT_NOT_ALLOWED
Code: FORBIDDEN
캘린더, 드라이브, 메일은 Service Account로 접근이 안 된다. 사용자 본인의 OAuth 토큰이 필요하다. 여기서 CLI 설계가 갈렸다.
CLI에서 User OAuth를 하려면 로컬에 HTTP 서버를 하나 띄워야 한다. http://localhost:9876/callback으로 리다이렉트 받아서 Authorization Code를 캡처하고, 토큰으로 교환하는 방식이다. Developer Console에서 이 Redirect URL을 미리 등록해둬야 한다.

결과적으로 nworks는 두 가지 로그인을 다 지원한다.
# 봇 메시지용 (Service Account)
nworks login

# 캘린더/드라이브/메일용 (User OAuth)
nworks login --user


scope 의존성: 문서에 안 쓰여있는 것
User OAuth 로그인할 때 scope를 지정한다. 캘린더를 쓰려면 calendar, 드라이브를 쓰려면 file 이런 식이다.
그런데 이게 단순하지가 않다.
할 일(task)을 만들려면 할당자(assignorId)가 필수다. 이걸 자동으로 채우려고 /users/me API를 호출하는데, 이 API는 user.read scope가 있어야 한다. 그래서 task scope만 줘서 로그인하면 이런 에러가 난다.
Error: has not permission api scope
Code: FORBIDDEN
task + user.read를 같이 줘야 한다.
캘린더도 비슷하다. 일정을 만들려면 calendar scope가 필요한데, 일정을 만든 뒤 확인하려면 calendar.read도 있어야 한다. calendar만 있으면 만들 수는 있는데 조회가 안 된다.
이런 의존성이 문서에 명시적으로 정리돼있지 않다. 다 직접 부딪혀서 알아낸 거다.
최종적으로 nworks에서 정리한 scope 의존성 맵은 이렇다.
| 기능 | 필요한 scope | 숨은 의존성 |
|---|---|---|
| task create/update/delete | task | user.read (할당자 조회) |
| calendar create/update/delete | calendar | calendar.read (생성 후 조회) |
| drive upload | file | - |
| mail send | - |
nworks는 이걸 자동 확장으로 해결했다. 사용자가 task만 요청해도 내부적으로 user.read를 추가한다. 사용자가 scope 의존성을 알 필요가 없다.
boardId int64 overflow: JavaScript의 함정
게시판 API를 붙이다가 이상한 일이 생겼다. 게시판 목록을 조회하면 boardId가 돌아오는데, 이걸 가지고 글 목록을 조회하면 다른 게시판의 글이 나왔다.
원인은 JavaScript의 숫자 정밀도였다.
NAVER WORKS API가 돌려주는 boardId가 7895647055266594817 같은 큰 숫자다. JSON.parse가 이걸 처리하면서 정밀도가 깨졌다.
JSON.parse('{"boardId": 7895647055266594817}')
// boardId: 7895647055266594816 ← 마지막 자리가 바뀜
1이 0으로 바뀌면서 완전히 다른 ID가 됐다. 분명 코드는 맞는데 엉뚱한 게시판이 나오니까 한참 헤맸다.
해결은 JSON 파싱 전에 큰 숫자를 문자열로 변환하는 헬퍼 함수를 만드는 거였다.
function safeParseJson(text) {
// 14자리 이상 숫자를 문자열로 변환
const safe = text.replace(/:(\s*)(\d{14,})/g, ':"$2"');
return JSON.parse(safe);
}
이 방식은 NAVER WORKS API처럼 응답 구조가 단순한 경우에는 충분하다. 다만 JSON 값 안에 큰 숫자 문자열이 섞여있으면 오작동할 수 있다. 엄격한 파싱이 필요하면 json-bigint 같은 라이브러리를 쓰는 게 맞다.
NAVER WORKS API를 쓸 때 ID 값이 큰 숫자로 오는 경우가 있다. boardId가 대표적이다. JavaScript에서 이걸 처리할 거면 JSON.parse를 그냥 쓰면 안 된다.
CLI 구조: commander + tsup
CLI 프레임워크는 commander를 썼다. nworks [도메인] [동작] 패턴에 딱 맞았고, 타입스크립트 호환도 깔끔했다. CLI가 복잡해질수록 유지보수가 어려워서 초반에 단순한 구조를 선택한 거다.
nworks message send --to <userId> --text "메시지"
nworks calendar list
nworks drive upload --file ./report.pdf
빌드는 tsup(내부적으로 esbuild)을 썼다. TypeScript → JavaScript 변환 + 번들링을 한 번에 해준다.
{
"scripts": {
"build": "tsup src/index.ts src/mcp.ts --format esm --dts"
}
}
진입점이 두 개다. index.ts는 CLI용, mcp.ts는 MCP 서버용. 하나의 패키지에서 두 가지 모드를 지원한다. 처음부터 AI를 위한 툴로 개발하기 위해서 MCP 확장을 염두에 두고 진입점을 분리해뒀다.
npm 배포: 배포 전 알아 둘 것
package.json 설정
npm에 CLI로 배포하려면 bin 필드가 필요하다.
{
"name": "nworks",
"version": "1.1.1",
"bin": {
"nworks": "dist/index.js"
},
"type": "module",
"files": ["dist"]
}
files에 dist만 넣으면 빌드 결과물만 배포된다. 소스 코드가 npm에 올라가지 않는다.
2FA (Two-Factor Authentication)
npm은 publish할 때 2FA를 강제한다. 문제는 기기를 바꾸거나 인증 앱을 초기화하면 2FA 복구가 필요하다는 거다. npm support에 티켓을 넣어야 하고, 며칠 걸린다.
배포 전에 npm 2FA가 정상 동작하는지 먼저 확인하는 게 좋다.
npm whoami
npm profile get

2FA 설정이 안되어있다면 npm의 Account Settings 에서 설정할 수 있다.

npx 지원
npx nworks로 설치 없이 실행하려면 bin 필드만 잘 설정하면 된다. 별도 작업 필요 없다. npm이 bin에 등록된 명령어를 자동으로 npx에서 실행 가능하게 만든다.
버전 관리
README만 바꿔도 npm에 반영하려면 버전을 올려야 한다. npm은 같은 버전으로 재배포가 안 된다.
npm version 1.1.1 --no-git-tag-version
npm run build
npm publish
환경 변수와 .env
CI/CD에서 쓰려면 환경 변수로 인증 정보를 넣을 수 있어야 한다.
NWORKS_CLIENT_ID=...
NWORKS_CLIENT_SECRET=...
NWORKS_BOT_ID=...
로컬 개발할 때는 .env 파일을 자동으로 읽도록 dotenv를 넣었다. nworks login 할 때 .env에 있는 값은 다시 물어보지 않는다.
GitHub Actions에서 배포 알림을 보내는 예시:
nworks message send --channel $CHANNEL_ID --text "v${VERSION} 배포 완료"
지금 다시 한다면
CLI 개발도 결국 UX가 중요하다. 내 경우에는 처음 만들어봐서 만들어 보고 직접 해보면서 수정을 거쳤는데 설계 단계에서 잘 잡혀있으면 더 깔끔한 결과가 나왔을 것 같다.
- 인증 구조를 처음부터 User OAuth 중심으로 설계했을 것이다. Service Account는 봇 메시지에만 쓰이고, 나머지 전부는 User OAuth다. 처음에 Service Account로 시작한 게 오히려 복잡도를 높였다. 이 과정을 수정하느라 초반에 좀 불필요한 패치가 있었다.
- scope 의존성도 처음부터 자동 확장으로 가야 한다. 사용자한테 "task 쓰려면 user.read도 추가하세요"라고 요구하는 건 나쁜 UX다.
- boardId int64 문제는 미리 알 수 없었다. 하지만 외부 API에서 ID 값을 받아서 쓸 때는 항상 정밀도를 의심하는 습관이 필요하다.
배운 것들
- 국내 API 문서를 너무 믿지 마라. scope 의존성 같은 건 직접 부딪혀야 알 수 있다.
- 외부 API에서 큰 숫자 ID가 오면 JSON.parse를 의심하라.
- npm 배포는 2FA가 필수고, 요즘은 대부분 사이트들이 2FA를 강제하는 것 같으니 잘 관리 할 것.
- CLI에서 User OAuth를 하려면 localhost callback 서버가 필요하다.
다음 글에서는 이 CLI를 MCP 서버로 확장하고, Glama에서 AAA 점수를 받고, awesome-mcp-servers에 등록하는 과정을 정리해볼 예정이다.
GitHub: https://github.com/yjcho9317/nworks
npm: https://www.npmjs.com/package/nworks
'개발기' 카테고리의 다른 글
| MCP AI 에이전트 보안 프록시 개발기 (4) — YAML 정책 엔진과 SARIF 감사 로깅 (0) | 2026.04.14 |
|---|---|
| MCP AI 에이전트 보안 프록시 개발기 (3) — 유니코드 호모글리프와 ReDoS로 보안 감사하기 (0) | 2026.04.14 |
| MCP AI 에이전트 보안 프록시 개발기 (2) — regex로 프롬프트 인젝션 탐지하기 (1) | 2026.04.14 |
| MCP AI 에이전트 보안 프록시 개발기 (1) — MCP 서버 응답은 검증 없이 AI에 전달된다 (0) | 2026.04.14 |
| NAVER WORKS CLI + MCP 서버 개발기 (2) — MCP 서버 만들고 awesome-mcp-servers 등록하기 (0) | 2026.03.20 |
