트랙
/
R
R
/
연습 문제
/
미용실 예약하기
미용실 예약하기

미용실 예약하기

학습 연습 문제

소개

"날짜와 시간은 어린아이에게도 가르치는 거예요. 그게 얼마나 어렵겠어요?"

많은 프로그래머가 그런 실수를 하고, 그 뒤에 겪는 일은 건강과 행복에 좋지 않은 영향을 주곤 해요.

날짜와 시간을 다루는 제법 복잡한 프로그래밍을 하는 사람이라면, 적어도 잠재적인 문제를 이해하고 완화할 준비는 되어 있어야 해요.

이 문서는 R이 날짜와 시간을 다루는 방식에 대한 개요를 전하려고 해요. 하지만 더 자세한 내용은 전체 문서를 참고해야 해요.

데이터 과학 언어인 만큼, R에서 날짜와 시간은 특히 중요해요. 많은 데이터가 시계열 형태로 들어오죠. 질병이 유행하는 동안의 일별 환자 수, 정부가 보고하는 월별 고용 지표, 번식지의 조류 개체 수, 주요 경제국의 연간 GDP 등 예는 끝이 없어요.

그렇다면 R이 날짜와 시간 처리를 간단하게 만들어 줄까요?

안타깝지만 아니에요!

Base R의 날짜와 시간 기능은 평판이 좋지 않고, 대부분은 이제 거의 쓰이지 않아요.

lubridate 패키지는 훨씬 나아졌지만, 크고 복잡한 문제를 폭넓게 다루도록 설계된 크고 복잡한 라이브러리에 대비해야 해요.

원래는 독립적이었던 lubridate는 이제 tidyverse 모음에 함께 묶여 있어요. library(lubridate) 또는 library(tidyverse)로 불러와요.

lubridate를 다루는 데 도움이 되는 자료는 다음과 같아요:

날짜와 날짜시간

date는 시간 정보 없이 그저 하루를 나타내요.

내부적으로 날짜는 1970-01-01 이후의 (부호 있는) 일수로 저장돼요.

today()
#> [1] "2026-04-22"

datetime은 날짜, 시간, 시간대 정보를 함께 담아요.

내부적으로는 UTC 시간대에서 1970년 시작 이후의 초 수로 저장돼요.

now()
#> [1] "2026-04-22 15:37:11 MST"  # mid-afternoon in Arizona

놀랍게도 R에는 시간만 나타내는 값에 대한 내부 표현이 없어요.

date와 datetime 값 다루기

아마도 가장 자주 필요한 날짜/시간 작업은 이런 것들이에요:

  • 적절한 입력 형식을 파싱해 날짜나 날짜시간을 만들어요.
  • 날짜나 날짜시간에서 필요한 숫자나 문자열 형식을 얻어요.
  • 날짜나 날짜시간 값에 오프셋을 적용해요.
  • 그런 값 두 개 사이의 시간 범위를 계산해요.
  • 현재 날짜(today())나 현재 날짜시간(now())을 가져와요.

문자열에서 파싱하기

날짜와 시간을 쓰는 방법은 많고, 대개 문화권마다 달라요. "7/6/23"처럼 숫자로만 된 날짜는 모호하고 헷갈리며, 다국적 기업에서 값비싼 실수를 많이 불러왔어요 (저자는 쓰라린 경험에서 우러나 이 말을 해요).

국제 표준(y-m-d)은 ISO 8601에 정의되어 있고, 두 가지 큰 장점이 있어요:

  • 파싱이 빠르고 명확해요.
  • 날짜시간을 일반 텍스트처럼 다뤄 알파벳순으로 정렬할 수 있어 정렬이 쉬워요.

하지만 데이터 과학은 구할 수 있는 데이터를 다루는 일이고, 남들이 데이터를 어떻게 형식화하는지 불평하는 건 대개 소용없어요.

Base R에는 형식 문자열을 받는 변환 함수가 있어요(기본값은 ISO 8601).

as.Date("2026-04-22")
#> [1] "2026-04-22"
as.Date("4/22/26", format = "%m/%d/%y")
#> [1] "2026-04-22"
as.POSIXct("2026-04-22 15:30:09 MST")
#> [1] "2026-04-22 15:30:09 MST"

형식 문자열은 수십 년 된 strptime() 함수와 비슷한데, 이 함수는 많은 언어에 들어 있어요. 이 문법에 익숙한 프로그래머라면 lubridate의 fast_strptime() 함수를 선택적으로 사용할 수 있어요.

더 유용하게도, lubridate는 필드 순서를 나타내는 이름을 가진 다양한 도우미 함수를 제공해요. 함수가 어떤 값을 어떤 순서로 찾아야 하는지 알기만 하면, 파싱은 놀랄 만큼 유연해요.

mdy("4/22/26")
#> [1] "2026-04-22"
mdy("April 22nd 26")
#> [1] "2026-04-22"
mdy("Wednesday, April 22nd 2026")
#> [1] "2026-04-22"
mdy_hm("Wednesday, April 22nd 2026, 4:14 pm")
#> [1] "2026-04-22 16:14:00 UTC"  # defaults to your OS timezone

숫자 값으로 만들기

프로그램이 날짜/날짜시간의 구성 요소를 각각 따로 숫자로 가지고 있는 경우에는 make_date() 또는 make_datetime()으로 이들을 조립할 수 있어요.

다음은 제공할 수 있는 구성 요소로, 순서와 기본값을 보여줘요:

make_datetime(
  year = 1970L,
  month = 1L,
  day = 1L,
  hour = 0L,
  min = 0L,
  sec = 0,
  tz = "UTC"
)
#> [1] "1970-01-01 UTC"


make_datetime(2026, 4, 22, 17)
#> [1] "2026-04-22 17:00:00 UTC"

당연히 make_date()는 연, 월, 일 인자만 받아요.

사람이 읽을 수 있는 형태로 출력하기

R 콘솔은 날짜/날짜시간을 자동으로 문자열 표현으로 보여줘요. 대개는 편리하지만, 때로는 오해를 불러일으켜요.

내부적으로 날짜와 날짜시간은 문자열이 아니라 숫자(그리고 몇 가지 속성)예요. 문자열이 기대되는 자리에서 날짜(시간)를 반환하면 문제가 생기니, 변환하는 방법을 알아야 해요.

간단한 방법은 Base R의 as.character()를 쓰는 것이고, 이 함수는 ISO 8601 형식의 문자열을 출력해요.

날짜시간을 위한 format_ISO8601() 함수도 있는데, 원하는 정밀도로 반올림하는 옵션이 있어요.

today() |> as.character()  # a date
#> [1] "2026-04-23"
now() |> as.character()  # a datetime
#> [1] "2026-04-23 09:07:09.779319"
now() |> format_ISO8601() # defaults to nearest second
#> [1] "2026-04-23T09:07:38"

ISO 8601 출력을 보고 싶어 하는 비프로그래머는 거의 없으니, 문화권별 날짜/시간 형식을 다루는 것이 꼭 필요해요.

Base R은 format() 함수를 제공하고, 이 함수는 다른 여러 언어에 들어 있는 유서 깊은 strftime() 함수와 비슷한 형식 문자열을 사용해요.

format(today(), "%A, %d %B %Y")
#> [1] "Thursday, 23 April 2026"

그 Base R 방식은 못생겼지만 믿을 만해요. 다음은 더 흔히 쓰이는 코드 몇 가지예요:

코드 결과
%d 월의 일 (5, 17, 28 등)
%a 요일 약어 (Mon, Tue, Wed 등)
%A 요일 전체 이름 (Monday, Tuesday 등)
%w 요일 번호 (0-6, 일요일이 0)
%u 요일 번호 (1-7, 월요일이 1)
%b 월 약어 (Jan, Feb 등)
%B 월 전체 이름 (January, February 등)
%y 두 자리 연도 (예: 89)
%Y 네 자리 연도 (예: 1989)
%h 시 (24시간제)
%m 분
%s 초
%z UTC로부터의 오프셋
%Z 시간대 (문자)

Lubridate는 stamp() 함수와 더 좁은 범위의 stamp_date(), stamp_time()으로 더 사람 친화적인 대안을 제공하려고 해요.

이 함수들은 원하는 출력의 예시인 템플릿 문자열을 받아, 그 형식을 만들어 내는 사용자 정의 함수를 만들어요.

Lubridate는 형식을 추측하려고 하지만, 자주 모호함에 부딪혀 잘못된 결과를 내놓아요. 모호함을 없애려면 형식 문자열(예: "dmy")을 제공할 준비를 해요.

Exercism에서는 시스템 로케일을 우회해 영어로 작업하기 위해(테스트가 기대하는 대로) locale = "C"를 지정하는 것도 유용해요. 로케일은 복잡해요!

sf <- stamp("Saturday, June 27, 2026 at 14:45", locale = "C")
#> Multiple formats matched: "%A, %B %d, %Y at %H:%M"(1), "Saturday, %Om %d, %Y at %H:%M"(1), "Saturday, %B %d, %Y at %H:%M"(1), "%A, %Om %d, %Y at %H:%M"(0)
#> Using: "%A, %B %d, %Y at %H:%M"

sf(make_datetime(2023, 3, 29, 15, 0, 0))
#> [1] "Wednesday, March 29, 2023 at 15:00"

구성 요소 설정하거나 추출하기

Lubridate는 날짜나 날짜시간의 요소를 가져오거나 설정하는 많은 함수를 제공해요. year()나 second()처럼 뻔한 것도 있고, quarter()나 epiweek()처럼 더 전문적인 것도 있어요.

이런 함수는 모두 값을 가져오는 데 쓸 수 있고, 대부분은 값을 설정하는 데도 쓸 수 있어요.

dt <- now()
dt
#> [1] "2026-04-23 15:47:11 MST"

# get
month(dt)
#> [1] 4

# set
month(dt) <- 11
dt
#> [1] "2026-11-23 15:47:11 MST"

wday()(요일)와 month()의 기본 출력은 정수지만:

  • labels = TRUE로 설정하면 현재 로케일에 해당하는 약어 문자열을 반환해요.
  • abbr = FALSE도 함께 설정하면 전체 이름을 반환해요.
d <- today()
d
#> [1] "2026-04-23"
wday(d)
#> [1] 5
wday(d, label = TRUE)
#> [1] Thu
#> Levels: Sun < Mon < Tue < Wed < Thu < Fri < Sat
wday(d, label = TRUE, abbr = FALSE)
#> [1] Thursday
#> Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < Friday < Saturday

이 레이블은 categorical variables이고, 다른 개념에서 다룰 주제예요. 값은 나열된 Levels로 제한되고, 이 레벨은 정렬 순서도 보여줘요.

주는 기본적으로 일요일에 시작하지만, week_start 인자로 바꿀 수 있어요(월요일은 1).

dt <- make_datetime(2026, 4, 23)

# default behavior
wday(dt)
#> [1] 5
wday(dt, label = T)
#> [1] Thu

# set week_start to Monday
wday(dt, week_start = 1)
#> [1] 4

몇몇 유틸리티 함수는 TRUE/FALSE 값을 반환해요. leap_year()나 dst()(일광 절약 시간이 적용 중인가?) 같은 것들이에요.

달마다 길이가 다르므로, days_in_month()가 정수 값을 알려줘요.

시간 범위

date와 datetime 값은 모두 타임라인 위의 한 시점을 나타내요.

"어제 이맘때"나 "지금부터 일주일 뒤" 같은 개념은 어떻게 다룰까요?

다른 과학 지향 언어에 익숙한 사람이라면 Python의 datetime.timedelta나 Julia의 Period 타입을 알고 있을지도 몰라요.

이와 달리 R에는 시간 범위를 나타내는 방법이 무려 세 가지나 있고, 각각 장점과 한계가 달라요. 늘 그렇듯, 상황에 가장 잘 맞는 것을 골라요.

여러 범위는 as.period(), as_duration(), as.interval()로 쉽게 서로 변환할 수 있어요.

범위 유형은 is.period() 등으로 확인할 수 있어요.

기간

period(예: hours(3))는 시계 시간을 3시간 늘리는 데 쓸 수 있어요.

일광 절약 시간(DST) 같은 불규칙성은 대체로 무시해요.

모든 기간은 단위 이름을 복수형으로 쓴 것이에요: years, weeks 등.

d <- today()
d
#> [1] "2026-04-25"

d + weeks(2)
#> [1] "2026-05-09"

# dates are coerced to datetimes at midnight if necessary
d - hours(20)
#> [1] "2026-04-24 04:00:00 UTC"

hours(3)
#> [1] "3H 0M 0S"

지속시간

duration(예: dhours(3))은 물리적 시간을 3시간 늘리고, 시계 시간이 물리적 시간에서 벗어나게 만드는 DST 같은 불규칙성도 반영해요.

모든 지속시간은 대응하는 기간에 d 접두사를 붙여요: dyears, dweeks 등.

dhours(3)
#> [1] "10800s (~3 hours)"

구간

interval은 시작 시간과 끝 시간의 조합이고, interval() 함수나 %--% 중위 연산자로 만들어요.

# the next 2 lines were run about 33s apart, so now() is different
dt1 <- now()
dt2 <- now() + hours(1)

# interval(0 function)
ivl <- interval(dt1, dt2)
ivl
#> [1] 2026-04-24 16:24:54 MST--2026-04-24 17:25:27 MST

# infix operator
iv2 <- dt1 %--% dt2
iv2 == iv1
#> [1] TRUE

시간대

시간대를 다루는 일은 까다로울 수 있어요. 아래 인용문은 Python 문서에서 온 것이지만, R에도 똑같이 해당해요:

"전 세계의 시간 조정 규칙은 합리적이라기보다 정치적이고, 자주 바뀌며, UTC 말고는 모든 애플리케이션에 맞는 표준이 없어요."

시간대에 관한 몇 가지 일반적인 점은 이래요:

  • datetime은 항상 시간대(tz)를 포함하지만, date는 시간대를 인식하지 않아요.
  • 날짜시간 생성자는 대체로 UTC를 기본값으로 해요.
  • now() 함수는 예외예요. 시스템 시계에서 tz를 포함한 로컬 시간을 가져와요. 기본적으로 로컬 tz를 쓰지만, now("UTC")처럼 다른 시간대를 지정할 수도 있어요.
# timezone defaults to UTC
local <- ymd_hm("2026-04-25 10:32")
local
#> [1] "2026-04-25 10:32:00 UTC"

# timezone can be specified
AZ <- ymd_hm("2026-04-25 10:32", tz = "America/Phoenix")
AZ
#> [1] "2026-04-25 10:32:00 MST"

# same clocktime, different tz
local - AZ
#> Time difference of -7 hours

시간대와 관련된 계산은 기간보다 지속시간을 쓸 때 더 믿을 만해요.

마무리

날짜와 날짜시간을 다루는 일은 대개 Base R보다 lubridate를 쓸 때 훨씬 쉬워요.

이 개념은 개요를 전하려고 했지만, 아주 많은 세부 사항을 어쩔 수 없이 빼놓았어요.

반복을 무릅쓰고 말하면, 문서가 친구예요. 그러니 꼭 활용해요.

지침

이 연습 문제에서는 2012년 9월 15일에 문을 연 뉴욕의 한 미용실을 위한 예약 스케줄러를 만들어요.

다섯 개의 과제가 있고, 모두 예약 날짜와 관련되어 있어요. 시간은 24시간 형식이며 AM/PM은 쓰지 않아요. 시간대 문제를 최대한 단순하게 하기 위해, 모든 시간이 UTC를 사용한다고 가정해요.

1. 숫자로만 된 예약 날짜 파싱하기

텍스트로 표현된 예약 날짜를 그에 해당하는 DateTime 형식으로 파싱하는 schedule_appointment() 함수를 구현해요.

입력은 미국식 월/일/년 순서라는 점에 유의해요.

julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00

2. 예약이 이미 지났는지 확인하기

예약 날짜를 받아서 그 예약이 과거에 있었는지 확인하는 has_passed() 함수를 구현해요.

julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true

3. 예약이 오후인지 확인하기

예약 날짜를 받아서 그 예약이 오후(12:00 이상 18:00 미만)인지 확인하는 is_afternoon_appointment() 함수를 구현해요.

julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true

4. 예약의 요일 확인하기

요일을 숫자로 반환하는 day_of_week() 함수를 구현해요. 미용실은 일요일에는 문을 닫아요. 따라서 월요일이 1이고 토요일이 6이에요.

# a Friday
day_of_week(make_datetime(2026, 06, 05, 12, 30, 00))
#> [1] 5

5. 예약 일정 변경하기

고객이 예약 일정을 변경해야 할 때는 다음 주 금요일 같은 시간으로 자동 변경돼요 (한 주 중 가장 한산한 날이기 때문이에요).

원래 예약(월/일/년 순서의 텍스트 문자열)을 받아서 변경된 예약을 반환하는 reschedule() 함수를 구현해요.

# a Saturday
reschedule("06/13/2026 12:30:00")
#> [1] "2026-06-19 12:30:00 UTC"

원래 예약이 월요일부터 목요일 사이라면 이번 주 금요일로 변경해요. 금요일이나 토요일 예약이라면 다음 주 금요일로 변경해요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요
R Exercism

미용실 예약하기 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 R 트랙을 개념 21개연습 문제 111개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.