맥을 켜면 알아서 떠 있어야 하는 것들이 있다. 봇, 로컬 서버, 백업 스크립트 같은 것들이다. 터미널에서 실행해두면 창을 닫는 순간 끝나고, 재부팅하면 당연히 사라진다.
macOS에서 이걸 맡는 건 launchd다. 리눅스의 systemd에 해당한다. 설정은 파일 하나면 되는데, 틀려도 아무 말을 안 해주는 게 문제다. 등록은 됐다고 하고, 실행은 안 되고, 에러도 없다. 이 글은 설정하는 법과 그 침묵을 깨는 법을 같이 적었다.
두 자리가 있다, 권한이 갈린다
넣는 위치가 두 곳이고 성격이 완전히 다르다.
~/Library/LaunchAgents— 내 홈 밑이다. 관리자 권한이 필요 없다. 내가 로그인할 때 뜬다./Library/LaunchDaemons— 시스템 자리다. 관리자 권한이 필요하고, 로그인 전에도 뜬다.
개인 작업은 앞쪽이면 충분하다. 회사 장비처럼 관리자 비밀번호가 없는 기계에서도 앞쪽은 쓸 수 있다. 인터넷 글을 따라 하다가 뒤쪽 경로에 넣으라는 안내를 보고 막히는 경우가 많은데, 대부분 앞쪽으로 바꾸면 그대로 된다.
파일 하나 만들기
~/Library/LaunchAgents/com.example.mybot.plist를 만든다. 이름은 자유지만 점으로 구분된 형태를 쓰는 게 관례다.
키 네 개만 알면 된다.
RunAtLoad— 등록할 때, 그리고 로그인할 때 바로 실행한다
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.mybot</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/me/scripts/run-bot.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/me</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/me/launch.log</string>
<key>StandardErrorPath</key>
<string>/Users/me/launch.log</string>
</dict>
</plist>
KeepAlive— 프로세스가 죽으면 다시 띄운다EnvironmentVariables— 환경 변수를 직접 넣는다StandardOutPath— 출력과 에러를 파일로 남긴다
등록하고 확인한다.
두 번째 명령에 이런 줄이 나오면 돌고 있는 것이다.
37257 -15 com.example.mybot
맨 앞이 프로세스 번호다. 여기가 -면 지금 안 돌고 있다는 뜻이다. 가운데는 마지막 종료 코드다.
안 될 때 확인할 것
여기부터가 이 글의 본론이다. 순서대로 짚으면 대부분 걸린다.
등록했는데 아무 일도 안 일어난다
제일 흔하고 제일 당황스러운 경우다. launchctl load를 쳤는데 에러도 없고 프로세스도 없다.
십중팔구 파일 문법 오류다. XML 태그 하나만 어긋나도 launchd는 그 파일을 통째로 무시하는데, 그러면서 아무 말도 안 한다. 확인하는 명령이 따로 있다.
plutil -lint ~/Library/LaunchAgents/com.example.mybot.plist
멀쩡하면 이렇게 나온다.
/Users/me/Library/LaunchAgents/com.example.mybot.plist: OK
틀렸으면 줄 번호까지 찍어준다. 실제로 <true/>를 <true>로 잘못 쓰고 돌려봤다.
/tmp/bad.plist: (Encountered non-empty <true> on line 15)
몇 시간 헤맬 일을 한 줄로 끝낸다. 파일을 만들거나 고칠 때마다 먼저 이걸 치는 습관을 들이는 게 낫다.

실행은 되는데 바로 죽는다
launchctl list에 나오는 종료 코드가 힌트다. 0이 아니면 실패한 것이다.
원인 대부분은 경로다. launchd는 우리가 터미널에서 쓰던 환경을 물려받지 않는다. python3이라고만 적으면 못 찾는다. which python3으로 확인한 전체 경로를 적어야 한다. 위 예시에서 PATH를 직접 넣은 것도 같은 이유다.
StandardErrorPath를 지정해두면 이 단계에서 바로 답이 나온다. 로그 파일을 열면 평소 터미널에서 보던 에러가 그대로 적혀 있다. 로그 경로는 처음부터 넣어두는 게 좋다. 문제가 생긴 뒤에 넣으려면 다시 등록해야 한다.
화면 관련 프로그램이 조용히 멈춘다
터미널 화면을 그리는 프로그램을 launchd로 띄우면 시작하다가 멈추는 일이 있다. 죽지도 않고 진행도 안 한다.
TERM과 LANG이 없어서다. 터미널에서 실행할 때는 셸이 넣어주는데 launchd 환경에는 없다. 스크립트 첫머리에 직접 넣는다.
export TERM="${TERM:-xterm-256color}"
export LANG="${LANG:-en_US.UTF-8}"
같은 이유로 확인 절차를 요구하는 프로그램도 위험하다. 사람에게 묻고 답을 기다리는데 답할 사람이 없으니 영원히 멈춘다. 자동 실행에 올리기 전에 사람 손이 필요한 구간이 없는지 봐야 한다.
고쳤는데 그대로다
스크립트를 수정했는데 동작이 안 바뀐다면, 이미 돌고 있는 건 옛 코드이기 때문이다. 파일을 고쳐도 실행 중인 프로세스는 갈아엎어지지 않는다.
다시 띄운다.
launchctl kickstart -k gui/$(id -u)/com.example.mybot
-k가 돌고 있는 것을 죽이고 다시 시작한다는 뜻이다. unload 후 load를 반복하는 것보다 간단하다.
KeepAlive가 못 잡는 것
KeepAlive는 프로세스가 사라졌을 때만 움직인다. 그래서 못 잡는 상태가 있다. 프로세스는 멀쩡히 살아 있는데 일을 안 하는 경우다.
서버의 세션이 만료됐거나, API 사용량을 다 썼거나, 내부에서 어딘가 걸려 멈춘 경우가 그렇다. 프로세스 목록에는 보이고, launchctl list에도 번호가 찍힌다. launchd가 보기에는 아무 문제가 없다.
이걸 잡으려면 살아 있는지가 아니라 일을 하고 있는지를 보는 감시가 따로 필요하다. 마지막으로 일을 처리한 시각을 파일에 남기게 하고, 그 시각이 너무 오래됐으면 프로세스를 강제로 내리는 식이다. 내리고 나면 KeepAlive가 알아서 다시 띄운다.
우리도 이 구조를 쓴다. launchd가 감시 스크립트를 상시 실행하고, 그 스크립트가 30초마다 실제 동작 여부를 확인한다. 봇에 적용한 형태는 여기에 정리했다. 디스코드 봇 24시간 돌리기, 집 컴퓨터로 호스팅 비용 0원
여기에도 함정이 하나 더 있었다. 감시 스크립트가 네트워크 확인을 하면서 페이지 본문을 통째로 받고 있었는데, 회선이 느린 날 그 확인이 제한 시간에 걸렸다. 그래서 감시자가 여덟 시간 동안 재시작을 못 했다. 본문 대신 머리말만 받도록 바꾸니 0.5초로 끝났다. 감시하는 쪽이 무거우면 감시가 먼저 무너진다.
정리
- 파일은
~/Library/LaunchAgents에. 관리자 권한이 필요 없다
launchctl load ~/Library/LaunchAgents/com.example.mybot.plist
launchctl list | grep mybot
- 고칠 때마다
plutil -lint. 침묵하는 오류를 한 줄로 잡는다 - 경로는 전부 절대 경로.
PATH와 로그 경로는 처음부터 넣는다 - 고친 뒤에는
launchctl kickstart -k KeepAlive는 죽은 것만 살린다. 멈춘 것은 따로 감시해야 한다
여기 나오는 명령은 전부 맥에서 직접 돌려 확인했다. 출력 형태는 그대로이고 이름과 경로만 예시로 바꿨다.

'기타팁 및 문제 해결' 카테고리의 다른 글
| 내부 스크립트 외부 공개: 사용자 친화적 문서화로 전환 (0) | 2026.09.06 |
|---|---|
| 자동화 에이전트 조용한 실패, 외부 감시 스크립트로 해결 (0) | 2026.09.05 |
| 맥 환경변수 설정하는 법, zshrc에 넣었는데 안 먹을 때 (0) | 2026.09.03 |
| 맥에서 외장 SSD가 안 보일 때, 어디부터 봐야 하나 (0) | 2026.09.02 |
| 윈도우에서 npm 설치했는데 명령어가 안 먹을 때, PATH부터 확인 (0) | 2026.09.01 |