---
title: "코딩 에이전트의 할 일을 애플 미리알림에 (1) — 되는 것, 안 되는 것, 5초의 함정"
description: "코딩 에이전트의 작업 상황을 맥 미리알림 앱으로 실시간 동기화하는 과정에서 발견한 AppleScript의 제약 사항과 5초 지연 문제를 해결한 경험을 공유합니다. 섹션과 태그는 제어할 수 없지만 깃발 기능을 활용해 진행 상황을 관리할 수 있으며, 항목 참조를 변수에 담지 않고 한 번에 속성을 수정하는 방식으로 성능을 9배 이상 개선했습니다"
date: 2026-09-01
updated: 2026-09-01T07:30:11.634Z
tags: [applescript, macos, claude-code, automation]
canonical: https://blog.wooncloud.com/posts/claude-code-todo-apple-reminders-1
---

![되는 것과 안 되는 것](/images/posts/claude-code-todo-apple-reminders-1/73668a0a-fd8f-40a4-bc36-236b905711da.webp)

코딩 에이전트에게 여러 단계짜리 작업을 맡기면, 지금 몇 번째 단계를 하고 있는지가 터미널 스크롤 속에 묻힌다. 진행 상황이 보이지 않으니 끝날 때까지 기다리거나, 중간에 로그를 뒤져야 한다.

그래서 에이전트의 할 일 목록을 **맥 기본 미리알림 앱**에 실시간으로 띄우기로 했다. 별도 앱을 깔 필요가 없고, 아이폰에서도 그대로 보이고, AppleScript 로 제어할 수 있다는 세 가지가 이유였다.

결과부터 말하면 잘 돌아간다. 다만 가는 길에 **쓰기 한 번이 5초씩 걸리는 함정**이 있었고, 그 원인을 잘못 짚었다가 이분 탐색으로 다시 찾아냈다. 이 글은 그 탐색 기록이다. 실제 구현과 설정법은 다음 편에서 다룬다.

## 미리알림은 어디까지 스크립트로 제어되나

AppleScript 사전을 읽는 대신, 되는지 안 되는지를 하나씩 두들겨봤다. `sdef` 로 사전을 덤프하려 했더니 Xcode 가 필요하다며 거부당해서, 그냥 속성을 직접 조회하는 쪽이 빨랐다.

리스트 조회는 한 줄이면 된다.

```bash
osascript -e 'tell application "Reminders" to get name of every list'
```

항목의 속성 전체를 덤프하면 무엇을 만질 수 있는지가 한눈에 나온다.

```bash
osascript -e 'tell application "Reminders" to get properties of (first reminder of list "Inbox")'
```

```text
priority:0, body:missing value, id:x-apple-reminder://...,
name:샘플, due date:missing value, allday due date:missing value,
creation date:date ..., completed:false, remind me date:missing value,
modification date:date ..., flagged:false, completion date:missing value,
container:list id ...
```

이 목록이 곧 제어 가능한 범위다. 여기 있는 것만 읽고 쓸 수 있다.

### 되는 것

리스트 생성·삭제, 항목 생성·삭제, 이름·완료·깃발·우선순위 변경, 리스트 간 이동까지 전부 된다.

리스트 **색상**은 사전에 없는 것처럼 보이지만 실제로는 있다. 값 형식이 문서화가 잘 안 되어 있어 세 가지를 시도해봤는데, hex 문자열만 통했다.

```bash
# 통함
osascript -e 'tell application "Reminders" to set color of list "Inbox" to "#FF9500"'

# 안 통함 — missing value 로 남는다
osascript -e 'tell application "Reminders" to set color of list "Inbox" to "orange"'

# 에러 (-1700): RGB 리스트를 text 로 변환 못 함
osascript -e 'tell application "Reminders" to set color of list "Inbox" to {65535, 38000, 0}'
```

항목을 다른 리스트로 옮기는 `move` 도 정상 동작한다.

```bash
osascript -e 'tell application "Reminders"
  move (first reminder of list "A" whose name is "할 일") to list "B"
end tell'
```

### 안 되는 것

**섹션(칸반 열)** 은 불가능하다. 최근 macOS 미리알림에는 리스트를 열로 나누는 섹션 기능이 있지만, 스크립트로는 손댈 수 없다.

```bash
osascript -e 'tell application "Reminders" to get every section of list "Inbox"'
# syntax error: 클래스 이름을 예상했지만 식별자를 발견했습니다. (-2741)
```

`section` 클래스 자체가 사전에 없고, 위에서 본 항목 속성에도 섹션을 가리키는 필드가 없다(`container` 는 리스트만 가리킨다). 리스트를 묶는 그룹(폴더)도 마찬가지다. UI 에서 섹션을 만들어둬도 스크립트로 만든 항목을 그 열에 넣을 방법이 없다.

**태그**도 불가능하다. 이건 판정에 약간의 추리가 필요했다. 항목 속성에 태그 필드가 없는 건 확인했는데, 제목에 `#` 를 쓰면 미리알림이 태그로 파싱해줄 가능성이 남아 있었기 때문이다.

그래서 전체 항목을 훑어 `name` 에 `#` 가 든 것이 몇 건인지 셌다.

```bash
osascript -e 'tell application "Reminders"
  set withHash to 0
  set total to 0
  repeat with L in lists
    repeat with r in reminders of L
      set total to total + 1
      if (name of r) contains "#" then set withHash to withHash + 1
    end repeat
  end repeat
  return "전체 " & total & "건 중 name 에 # 포함 " & withHash & "건"
end tell'
```

결과는 **549건 중 1건**이었고, 그 1건은 방금 내가 테스트로 만든 항목이었다. 즉 UI 에서 태그를 단 기존 항목들은 `name` 에 `#` 가 남아 있지 않다 — 태그는 이름과 별개 저장소에 있고, 스크립트에는 노출되지 않는다. 반대로 내가 제목에 넣은 `#` 는 그냥 텍스트로 남았다. 파싱은 UI 입력일 때만 일어난다.

우회로 두 개도 막혔다. **EventKit** 직접 접근은 osascript 에서 권한 콜백이 아예 오지 않는다(TCC 승인 경로가 다르다).

```javascript
// osascript -l JavaScript 로 실행
ObjC.import('EventKit');
const store = $.EKEventStore.alloc.init;
let granted = false, done = false;
store.requestFullAccessToRemindersWithCompletion((g) => { granted = g; done = true; });
// 런루프를 8초 돌려도 done 이 false 로 남는다
```

미리알림 데이터베이스를 직접 읽는 것도 안 된다. `~/Library/Reminders` 아래에 sqlite 파일이 보이지 않는다.

정리하면 이렇다.

| 대상 | 가능 여부 |
|---|---|
| 리스트 생성·삭제·색상 | 가능 (색상은 `"#FF9500"` 같은 hex 문자열) |
| 항목 생성·삭제·이동 | 가능 |
| 이름·완료·깃발·우선순위 | 가능 |
| 섹션(칸반 열) | 불가 |
| 태그 | 불가 (읽기조차 안 됨) |
| EventKit / SQLite | 불가 |

칸반 열을 못 쓰는 건 아쉬웠는데, **깃발이 대안이 됐다.** 깃발은 리스트를 가로질러 사이드바의 "깃발 표시"에 모이기 때문에, 진행 중인 항목에만 깃발을 켜두면 그게 곧 "지금 뭘 하고 있나" 뷰가 된다. 세션을 여러 개 동시에 돌려도 각자의 현재 작업이 한 화면에 모인다.

## 5초의 함정

기능 탐색이 끝나고 실제 동기화 스크립트를 붙였더니, 항목 하나를 완료 처리하는 데 **5.3초**가 걸렸다. 다섯 개짜리 목록을 갱신하면 25초다. 초기 구현은 아예 20초 타임아웃에 걸려 항목이 절반만 반영되고 `-609`(연결이 유효하지 않습니다) 에러로 죽었다.

### 첫 번째 진단은 틀렸다

처음 세운 가설은 "완료 처리가 iCloud 동기화를 유발한다"였다. 그럴듯했다. 완료는 다른 기기에 즉시 반영되어야 하는 상태 변화니까.

이 가설을 반증한 건 **깃발**이었다. 깃발도 똑같이 동기화 대상인데 0.3초밖에 안 걸렸다.

```bash
# 0.30초
osascript -e 'tell application "Reminders" to set flagged of (first reminder of list "Inbox" whose body is "k") to true'
# 0.29초
osascript -e 'tell application "Reminders" to set priority of (first reminder of list "Inbox" whose body is "k") to 1'
```

같은 계정, 같은 리스트, 같은 항목인데 어떤 속성은 0.3초고 어떤 속성은 5초라면 동기화 탓이 아니다. 확인 사살로 완료 상태를 10번 연속 토글해봤다.

```bash
for i in $(seq 1 10); do
  v=$([ $((i % 2)) -eq 0 ] && echo false || echo true)
  osascript -e "tell application \"Reminders\" to set completed of (first reminder of list \"Inbox\" whose body is \"k\") to $v"
done
```

10회 전부 0.24~0.30초. **`completed` 는 범인이 아니었다.** 추정으로 원인을 적어둘 뻔했다.

### 이분 탐색

그렇다면 스크립트가 만들어내는 AppleScript 안에 범인이 있다는 뜻이다. 문장을 하나씩 걷어내며 시간을 쟀다.

```applescript
-- 1) 원본: 10.79초
tell application "Reminders"
  if not (exists list "Inbox") then make new list with properties {name:"Inbox"}
  set color of list "Inbox" to "#FF9500"
  set L to list "Inbox"
  set r to (first reminder of L whose body is "k")
  set name of r to "새 이름"
  set completed of r to true
end tell
```

```applescript
-- 2) exists 검사와 color 설정을 뺌: 10.09초  (거의 안 줄었다)
tell application "Reminders"
  set L to list "Inbox"
  set r to (first reminder of L whose body is "k")
  set name of r to "새 이름"
  set completed of r to true
end tell
```

```applescript
-- 3) name 설정도 뺌: 5.40초  (정확히 절반)
tell application "Reminders"
  set L to list "Inbox"
  set r to (first reminder of L whose body is "k")
  set completed of r to true
end tell
```

```applescript
-- 4) 참조를 변수에 담지 않고 한 문장으로: 0.25초
tell application "Reminders"
  set completed of (first reminder of list "Inbox" whose body is "k") to true
end tell
```

3번과 4번은 **논리적으로 완전히 같은 일**을 한다. 차이는 항목 참조를 변수 `r` 에 담았느냐뿐인데 21배 차이가 난다.

그리고 2번과 3번을 비교하면 패턴이 보인다. 변수를 통해 쓰는 속성이 하나 늘 때마다 5초씩 선형으로 증가한다. 속성 하나면 5.4초, 둘이면 10.1초.

원인은 이렇게 이해했다. `first reminder of L whose body is "k"` 를 변수에 담으면 그 변수는 값이 아니라 **참조(specifier)** 다. 그 참조로 속성을 쓸 때마다 앱이 조건을 다시 해석하고, 그 왕복에 고정 비용이 붙는다. 리스트를 변수에 담는 것(`set L to list "Inbox"`)은 무해했다 — 항목 참조만 그렇다.

### 처방

여러 속성을 바꿔야 하면 `set properties` 로 한 번에 쓴다. 참조를 변수에 담지 않는 것이 핵심이다.

```applescript
-- 0.37초. 이름·완료·깃발을 한꺼번에 바꾼다
tell application "Reminders"
  set properties of (first reminder of list "Inbox" whose body is "k") to ¬
    {name:"▶ 작업 중", completed:false, flagged:true}
end tell
```

이 한 줄로 바꾼 뒤 실제 경로를 다시 쟀다.

| 동작 | 수정 전 | 수정 후 |
|---|---|---|
| 항목 5개 등록 | 1.3초 | 1.3초 |
| 진행 중 표시 / 완료 체크 | 5.3초 | 0.5~0.65초 |

약 9배 빨라졌고, 20초 타임아웃 문제도 함께 사라졌다. iCloud 를 끄는 방법을 찾을 필요도 없어졌다. (참고로 끌 수도 없다. 미리알림은 리스트 단위로 계정을 고르는 수단이 없어서, 로컬 저장을 쓰려면 iCloud 미리알림 자체를 꺼야 한다.)

## 덤으로 걸린 것들

탐색 중에 두 번 헛짚었는데 둘 다 기록해둘 만하다.

첫째, 측정 루프가 조용히 실패했다. zsh 는 변수를 확장할 때 **단어 분리를 하지 않는다.**

```bash
# zsh 에서는 "done 1" 이 통째로 한 개의 인자로 전달된다
for c in "done 1" "doing 2"; do
  node script.mjs $c
done
```

bash 라면 `done` 과 `1` 두 개로 쪼개지지만 zsh 는 그러지 않는다. 스크립트는 인자를 못 알아듣고 usage 를 뱉었는데, 나는 시간만 뽑아 보고 있어서 그 메시지를 놓쳤다. 측정값이 0.03초로 나온 걸 보고서야 이상함을 알아챘다.

둘째, AppleScript 소스를 파이프로 넘기면 안 된다.

```bash
# syntax error (-2740)
node gen-script.mjs | osascript

# 파일로 저장해 실행하거나 -e 로 넘긴다
node gen-script.mjs > /tmp/op.applescript && osascript /tmp/op.applescript
```

## 정리

- 미리알림은 리스트·항목·완료·깃발·색상까지는 스크립트로 충분히 다룰 수 있다. 섹션과 태그는 포기해야 한다.
- 칸반 열을 못 쓰는 자리는 **깃발**이 대신할 수 있다. 리스트를 가로질러 모이므로 "지금 진행 중인 것" 뷰로 쓰기 좋다.
- 항목 참조를 변수에 담고 속성을 하나씩 쓰면 쓰기당 5초가 붙는다. `set properties`로 한 문장에 묶어라.
- 느린 이유를 추정으로 적어두지 마라. 그럴듯한 첫 가설(iCloud)은 인접한 다른 속성 하나를 재보는 것만으로 죽었다.

[다음 편](/posts/claude-code-todo-apple-reminders-2)에서는 이걸 실제 하네스로 조립한다. 훅 배선, 여러 세션이 리스트 하나를 공유할 때의 격리 설계, 세션이 끝나거나 비정상 종료됐을 때의 정리 전략을 다룬다.
