iCloud 캘린더 API를 앱에 통합하는 방법
목차
모든 캘린더를 위한 하나의 API 사용하기
단일 API를 사용하여 모든 캘린더 제공업체를 통합하려면 Unified Calendar API에 가입하세요. 신용카드는 필요하지 않습니다.
이전 기사에서는 Google Calendar API를 애플리케이션에 통합하는 방법을 설명하며 통합을 작동시키기 위한 모든 단계를 강조했습니다.
Google Calendar와 달리 iCloud Calendar를 통합하는 것은 그리 간단하지 않습니다. 공식 문서가 부족하고, Apple은 개발자가 iCloud Calendar를 애플리케이션에 통합하기 위해 해야 할 작업을 충분히 정리하지 않았기 때문입니다.
이 기사에서는 iCloud Calendar API를 애플리케이션에 통합하는 방법에 대해 심층적으로 다룰 예정이며, 인증 방식, 지원되는 작업, 제한 사항, 통합을 쉽게 해주는 도구들에 대해 설명합니다.
Apple iCloud Calendar는 어떤 프로토콜과 표준을 사용하나요?
Apple iCloud Calendar는 캘린더 통신을 위해 CalDAV 표준을 사용합니다. CalDAV는 서버에서 캘린더와 이벤트를 관리할 수 있도록 하는 WebDAV의 확장입니다.
이벤트는 ICS(iCalendar) 형식으로 표현되며, 이는 캘린더 데이터를 위한 텍스트 기반 형식입니다. 즉, 애플리케이션은 CalDAV 요청과 ICS 데이터를 사용하여 HTTP를 통해 iCloud 캘린더와 통신할 수 있습니다.
장점은 이것이 Apple 장치나 운영 체제에 종속된 구현이 아니라는 것입니다. 어떤 플랫폼에서 실행 중인 서버에서든 사용할 수 있습니다.
단점은 iCloud가 캘린더에 대한 REST API를 제공하지 않기 때문에, CalDAV만이 iCloud Calendar를 애플리케이션에 통합할 수 있는 유일한 방법이라는 점입니다.
CalDAV는 서버 간 통합에 적합하기 때문에, 우리는 서버 간 통합 방식만을 권장합니다.
iCloud는 어떻게 인증하나요?
일반적으로 플랫폼(Google Calendar 등)을 애플리케이션에 통합하려면 해당 플랫폼의 개발자 계정을 만들고, 애플리케이션을 생성하고, 스코프를 구성하고, 테스트 사용자를 추가하고, 애플리케이션 정보를 입력하고, 승인을 요청해야 합니다.
이 모든 단계를 거쳐 승인되면, 최종 사용자에게 플랫폼의 OAuth 화면이 표시되고, 사용자는 로그인한 후 애플리케이션이 요청하는 스코프에 명시적으로 접근 권한을 부여하게 됩니다. 이때 사용자에게는 애플리케이션 이름과 설정한 정보들이 표시됩니다.
Apple iCloud는 이러한 방식이 아닙니다. iCloud에는 Google Calendar나 Outlook처럼 표준 OAuth 흐름이 없습니다. 사용자의 iCloud Calendar에 연결하려면 SSL을 통한 Basic Authentication을 사용하여 Apple ID로 인증해야 합니다. 대부분의 iCloud 계정에는 2단계 인증이 활성화되어 있기 때문에, 사용자는 기본 Apple 계정 비밀번호 대신 앱 전용 비밀번호를 생성해야 합니다.
귀하의 애플리케이션은 사용자에게 iCloud 이메일(Apple ID)과 16자리 앱 전용 비밀번호를 입력하라고 요청해야 합니다. 이 자격 증명을 사용하여 iCloud의 CalDAV 서비스에 연결할 수 있습니다.
아래는 사용자가 이메일 주소와 앱 전용 비밀번호를 입력하도록 애플리케이션이 요청하는 예시입니다.

위에서 언급한 이유 외에도 Apple은 타사 캘린더 접근에 앱 전용 비밀번호를 요구함으로써 보안을 강화하고 있습니다. Apple iCloud 비밀번호를 제3자 애플리케이션과 공유하는 것은 바람직하지 않기 때문입니다.
기술적으로, HTTP 요청 헤더는 다음과 같이 Base64로 인코딩된 앱 전용 비밀번호를 포함하게 됩니다: Authorization: Basic <app-specific-password-here>
iCloud Calendar API는 어떤 메서드를 지원하나요?
iCloud의 CalDAV 서비스는 caldav.icloud.com에서 호스팅됩니다. 인증이 완료되면 아래와 같은 메서드를 사용할 수 있습니다:
- 사용자의 캘린더 나열
- 이벤트의 CRUD(Create, Read, Update, Delete)
- 특정 이벤트 가져오기
CalDAV 표준에 대해 더 알고 싶다면, RFC 4791을 읽어보세요. 여기에는 사용 가능한 모든 메서드, 필터 등이 설명되어 있습니다.
아래는 iCloud Calendar 통합을 위한 가장 중요한 정보 요약입니다.
지원되는 HTTP 메서드:
|
HTTP 메서드 |
CalDAV에서의 역할 |
iCloud 지원 여부 |
주의 사항 |
|---|---|---|---|
|
OPTIONS |
서버 기능 탐색 |
✅ |
디버깅에는 좋지만 런타임에는 필수 아님. |
|
PROPFIND |
주체 찾기, |
✅ |
먼저 인증 필요; Depth 0 또는 1 사용. |
|
MKCALENDAR |
새 캘린더 컬렉션 생성 |
✅ |
쓰기 권한 필요; Section 5.3.1 참고. |
|
REPORT |
데이터 쿼리( |
✅ |
세 가지 REPORT는 사양상 필수이며 iCloud에 존재함. |
|
PUT |
|
✅ |
VCALENDAR 전체 전송 필요; PATCH 불가. |
|
DELETE |
이벤트 또는 캘린더 삭제 |
✅ |
|
|
COPY / MOVE |
캘린더 간 이벤트 복사/이동 |
✅ |
PUT과 동일한 조건 적용. |
|
GET |
단일 |
✅ |
|
캘린더 컬렉션 속성
|
속성 |
목적 |
iCloud 특이 사항 |
|---|---|---|
|
|
사람이 읽을 수 있는 설명 |
완벽히 지원됨 |
|
|
쿼리의 기본 시간대 |
지원됨 |
|
|
캘린더가 수용 가능한 구성요소(VEVENT, VTODO) |
이벤트와 작업은 별도 캘린더로 구분됨. |
|
|
허용 MIME/버전(일반적으로 |
iCloud 기본값 사용 |
|
|
이벤트당 최대 바이트 |
iCloud 약 20MB 제한 |
|
|
서버 제한 사항 |
403/507 오류 방지를 위해 준수 필요. |
OneCal Unified Calendar API는 프로덕션 환경에서 바로 사용할 수 있습니다. 단일 API를 사용하여 모든 캘린더 제공업체를 통합하려면 무료로 가입하세요.
통합을 쉽게 해주는 라이브러리는 어떤 것이 있나요?
CalDAV, ICS, XML, 기타 iCloud Calendar 관련 특이 사항을 직접 처리하는 것은 복잡하고 시간이 많이 걸립니다. 각 메서드를 이해하고, XML을 JSON으로 변환하고, 애플리케이션에서 사용하는 데 많은 시간을 소모하게 됩니다.
보다 수월한 통합을 위해 아래 라이브러리 사용을 추천합니다:
tsdav: JavaScript/TypeScript를 사용하는 경우 필수입니다. tsdav를 사용하면 CalDAV 전용 문법이나 용어 없이도 iCloud 서버와 쉽게 통신할 수 있습니다. 이 라이브러리는 HTTP 메서드와 XML 요청을 고수준의 TypeScript API로 감싸서 처리합니다. 자세한 사용법은 tsdav 문서를 참조하세요.ical-generator: CalDAV를 통해 iCloud Calendar를 통합하면 이벤트 생성 또는 업데이트 시 전체 .ics 파일을 업로드 및 교체 해야 합니다. 이 파일을 수동으로 작성하는 것은 오류 가능성이 높으며, VEVENT마다 적절한 헤더, UID, DTSTART/DTEND 포맷, RRULE 문자열, 시간대 정보 등이 필요합니다. ical-generator는 이러한 문제를 해결해줍니다.ical.js: Mozilla Calendar 팀이 개발한 순수 JavaScript 파서/엔진으로, ICS 응답을 JS 클래스로 파싱할 수 있게 해줍니다.
아래 표는 tsdav가 iCloud Calendar 통합에서 어떤 역할을 수행하는지 설명합니다:
|
스택 내 역할 |
tsdav의 기능 |
iCloud에서의 중요성 |
|---|---|---|
|
CalDAV / WebDAV 클라이언트 |
모든 HTTP 메서드와 XML을 수동으로 작성하지 않아도 되도록 고수준 TypeScript API 제공 복잡한 XML 문자열 생성이나 multistatus 응답 파싱 없이 비즈니스 로직에 집중 가능 |
|
|
탐색 헬퍼 |
|
iCloud에 특화된 2단계 탐색 과정을 생략 가능 |
|
인증 래퍼 |
Basic 및 OAuth 2 인증에 대한 빌트인 헬퍼 포함. iCloud의 경우 |
Base64 인코딩 및 헤더 직접 작성 불필요 |
|
공통 작업에 대한 타입 헬퍼 |
|
RFC-4791 XML 문법에 대한 걱정 없이 CRUD 구현 가능 |
|
동기화 토큰 지원 |
|
iCloud는 푸시 기능이 없기 때문에, 이 기능을 활용한 폴링 구현이 간편함 |
|
브라우저 + Node 호환성 |
서버 코드(Node) 또는 브라우저에서도 작동함 (isomorphic fetch 사용) |
브라우저 확장 프로그램이나 SPA에서 일부 코드가 실행될 경우 유용 |
|
타입 정의 포함 최신 TS 프로젝트 |
전체 타입 정의 제공, ES 모듈 트리 쉐이킹 가능, 의존성 최소화 |
최신 빌드 파이프라인에 쉽게 통합 가능 |
iCloud Calendar를 통합하기 위한 tsdav + ts 사용 예시
이 예시에서는 사용자의 Apple ID 및 앱 전용 비밀번호를 이미 획득했다고 가정합니다.
import { DAVCalendar, DAVClient, DAVNamespaceShort, DAVObject } from "tsdav";
const APPLE_DAV_URL = "https://caldav.icloud.com";
function createClient({
username,
password,
}: {
username: string;
password: string;
}) {
const client = new DAVClient({
serverUrl: APPLE_DAV_URL,
credentials: {
username,
password,
},
authMethod: "Basic",
defaultAccountType: "caldav",
});
return client;
}
constants
/*
The prodId value is a required field that must appear on every calendar object.
The field is a globally-unique identifier for the software that
produced the file. tsdav might automatically generate that information for you,
but it might be best if you provide it manually.
You might find it useful when you have edge cases, you can use it to tell which
program wrote the data.
*/
export const PROD_ID = {
company: "your-company-name-here",
product: "your-product-name-here",
};getCalendars
async getCalendars(
...params: Parameters<typeof DAVClient.prototype.fetchCalendars>
): Promise<DAVCalendar[]> {
// You can abstract this initialization into another method.
// For the sake of simplicity, we'll initialize the client on each method.
const client = createClient({
username: <email-here>,
password: <password-here>,
});
return client.fetchCalendars(...params);
}getCalendarById
async getCalendarById(calendarUrl: string): Promise<DAVCalendar> {
const calendars = await getCalendars();
const calendar = calendars.find((el) => el.url === calendarUrl);
if (!calendar) {
throw new Error(`Apple Calendar with id ${calendarUrl} not found`);
}
return calendar;
}getCalendarEvents
async getCalendarEvents(
calendarUrl: string,
query: GetCalendarEventsQuery = {}
) {
const client = createClient({
username: <email-here>,
password: <password-here>,
});
const calendar = await getCalendarById(calendarUrl);
const events = await client.fetchCalendarObjects({
calendar,
timeRange: query.dateRange ?? undefined,
})
return { events, nextSyncToken: calendar.syncToken };
}getEventById
async getEventById(calendarUrl: string, eventId: string) {
const client = createClient({
username: <email-here>,
password: <password-here>,
});
const eventUrl = new URL(`${eventId}.ics`, calendarUrl).pathname;
const responses = await client.calendarMultiGet({
url: calendarUrl,
props: {
[`${DAVNamespaceShort.DAV}:getetag`]: {},
[`${DAVNamespaceShort.CALDAV}:calendar-data`]: {},
},
objectUrls: [eventUrl],
depth: "1",
})
if (responses.length === 0) {
throw new Error(
`Received no response while fetching ${eventUrl}`,
null
);
} else if (responses[0].status >= 400) {
throw new Error(
`Failed to get Apple event by id. Status: ${responses[0].statusText}`,
responses[0]
);
}
const response = responses[0];
const calendarObject: DAVObject = {
url: new URL(response.href ?? "", calendarUrl).href,
etag: `${response.props?.getetag}`,
data:
response.props?.calendarData?._cdata ?? response.props?.calendarData,
};
try {
return calendarObject;
} catch (err: any) {
this.logger.error("Failed to process Apple Response", {
message: err.message,
event: calendarObject,
});
return [];
}
}createEvent
async createEvent(calendarUrl: string, data: AppleEvent) {
const client = createClient({
username: <email-here>,
password: <password-here>,
});
const eventId = data.id ?? <generate-id-here>
const calendar = ical({
prodId: PROD_ID,
method: ICalCalendarMethod.REQUEST,
});
const event = calendar.createEvent({ ...data, id: eventId });
const response = await client.createCalendarObject({
calendar: {
url: calendarUrl,
},
filename: `${event.id()}.ics`,
iCalString: calendar.toString(),
})
if (!response.ok) {
throw new Error(
`Failed to create Apple event: ${response.statusText}`,
response
);
}
return { id: event.id(), eventWithExceptions };
}updateEvent
async updateEvent(calendarUrl: string, eventId: string, data: AppleEvent) {
const client = createClient({
username: <email-here>,
password: <password-here>,
});
const originalEventData = await getEventById(calendarUrl, eventId);
const calendar = ical({
prodId: PROD_ID,
method: ICalCalendarMethod.REQUEST,
});
for (let event of originalEventData) {
if (event.status === ICalEventStatus.CANCELLED) continue;
if (event.id === eventId) {
calendar.createEvent({ ...event, ...data, id: eventId, url: null });
} else {
calendar.createEvent({ ...event, id: eventId, url: null });
}
}
const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);
const response = await
client.updateCalendarObject({
calendarObject: {
url: calendarObjectUrl.href,
data: calendar.toString(),
},
})
if (!response.ok) {
throw new Error(
`Failed to update Apple event: ${response.statusText}`,
response
);
}
return getEventById(calendarUrl, eventId);
}deleteEvent
async deleteEvent(calendarUrl: string, eventId: string) {
const client = createClient({
username: <email-here>,
password: <password-here>,
});
const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);
const response = await client.deleteCalendarObject({
calendarObject: {
url: calendarObjectUrl.href,
},
})
if (!response.ok) {
throw new Error(
`Failed to delete Apple event: ${response.statusText}`,
response
);
}
return { id: eventId };
}Apple iCloud Calendar의 제한 사항은?
- REST API 또는 JSON 응답 없음: iCloud Calendar API와 통신하려면 CalDAV를 사용해야 하며 .ics 파일을 다루어야 합니다. 이는 Outlook Calendar API나 Google Calendar API가 JSON을 기반으로 하는 REST API를 제공하는 것에 비해 비생산적입니다. 앞서 추천한 라이브러리를 사용하면 다소 개선할 수 있지만, 사용하는 언어나 프레임워크에 따라 라이브러리의 가용성이 달라집니다.
- 문서 부족: 공식 CalDAV 문서를 링크하긴 했지만, iCloud Calendar API와 통신할 때 모든 메서드가 작동하는 것은 아니므로 수많은 테스트와 예외 처리를 염두에 두어야 합니다.
- 앱 전용 비밀번호를 사용하는 Basic Auth: iCloud는 표준 OAuth 2.0 방식을 따르지 않으며, 앱 전용 비밀번호를 사용하여 인증 및 통신해야 합니다.
- 웹훅/푸시 알림 미지원: Google Calendar나 Outlook과 달리, iCloud는 최고의 캘린더 API가 아닙니다. 캘린더 변경 사항에 대해 웹훅을 등록하거나 실시간 업데이트를 받을 수 없습니다. 해결책은 주기적으로 폴링하고
sync-collection리포트를 활용하여 변경 사항을 확인하는 것입니다. - PATCH 메서드 미지원: 이벤트를 부분적으로 업데이트할 수 없습니다. 대신 전체 PUT을 통해 이벤트를 갱신해야 합니다.
- 초대장 제어 불가: iCloud Calendar는 회의 초대장을 자동으로 처리합니다. 참석자가 있는 이벤트를 생성하거나 수정하면, iCloud는 초대장을 자동 전송하고 참석자의 상태를 업데이트합니다. CalDAV의 스케줄링 Outbox/Inbox를 통해 초대장을 수동으로 제어할 수 없습니다.
iCloud Calendar를 애플리케이션에 더 쉽게 통합하는 방법이 있을까요?
iCloud Calendar는 표준 캘린더 API 규약을 따르지 않고, 많은 CalDAV 메서드와 필터가 작동하지 않기 때문에 통합이 결코 쉽지 않습니다.
더 간단한 방법은 Unified Calendar API를 사용하는 것입니다.

Unified Calendar API 제품을 사용하면 다음과 같은 이점이 있습니다:
- 잘 문서화되고 테스트된 API를 사용하여 iCloud Calendar를 애플리케이션에 통합할 수 있습니다.
- 통합 개발 및 유지보수에 소요되는 시간을 줄일 수 있습니다. Unified Calendar API는 API, 클라이언트, 예외 처리 등 대부분의 사항이 준비되어 있으므로, 개발자 입장에서 훨씬 적은 시간으로 통합을 완료할 수 있습니다.
- iCloud 외의 캘린더 제공자(Google Calendar, Outlook 등)도 별도 작업 없이 통합 가능합니다. 대부분의 Unified Calendar API 제품은 주요 캘린더 제공자와 통합되어 있으므로 모든 제공자를 한 번에 통합할 수 있습니다.
- 커스텀 폴링 없이 웹훅/푸시 알림 기능 사용 가능. iCloud에 대한 자체 폴링 솔루션을 구축하는 것은 큐와 서버를 추가로 운영해야 하므로 부담이 큽니다.

iCloud Calendar 통합에는 Unified Calendar API를 사용하세요
OneCal Unified Calendar API 출시가 임박했습니다. 위에서 설명한 Unified Calendar API의 모든 이점을 제공할 예정입니다.
iCloud Calendar를 애플리케이션에 통합할 때 발생하는 다양한 문제를 직접 처리할 필요가 없습니다. 수년간의 경험과 버그 수정을 집약한 Unified Calendar API를 사용하세요. 신용카드 없이 무료로 가입할 수 있습니다.
FAQ
iCloud Calendar는 어떤 프로토콜을 사용하나요?
iCloud Calendar는 HTTP 기반의 CalDAV를 사용하며, 이벤트는 iCalendar(ICS) 형식으로 저장됩니다.
iCloud Calendar는 REST API를 제공하나요?
아니요. iCloud Calendar 데이터 읽기/쓰기를 위한 유일한 방법은 CalDAV입니다.
iCloud Calendar는 어떻게 인증하나요?
사용자의 Apple ID(일반적으로 이메일 주소)와 16자리 앱 전용 비밀번호를 SSL을 통한 Basic Auth로 전달합니다.
iCloud Calendar는 푸시 알림이나 웹훅을 지원하나요?
아니요. 변경 사항을 감지하려면 iCloud 서버를 주기적으로 폴링해야 합니다.
통합을 단순화하는 라이브러리는 무엇인가요?
tsdav(CalDAV 클라이언트), ical-generator(ICS 파일 생성기), ical.js(ICS 파서)는 대부분의 저수준 작업을 처리해줍니다.
CalDAV와 ICS를 직접 다루지 않으려면 어떻게 하나요?
iCloud, Google, Outlook을 하나의 현대적인 JSON 인터페이스로 감싸는 OneCal Unified Calendar API와 같은 Unified Calendar API를 사용하세요.