트랙
/
R
R
/
연습 문제
/
로그 레벨
로그 레벨

로그 레벨

학습 연습 문제

소개

배경

문자열은 따옴표 사이에 들어 있는 텍스트 조각이에요. 현재 지원되는 모든 R 버전은 문자열을 UTF-8로 인코딩하므로, 세상 대부분의 언어를 지원할 수 있어요.

mystring <- "some text"
mystring
#> [1] "some text"
typeof(mystring)
#> [1] "character"

아쉽게도, 다른 언어에 익숙한 프로그래머에게는 이 용어가 헷갈릴 수 있어요.

type은 character이지만, R은 한 글자(다른 여러 언어에서는 char라고 부르지만 R에서는 아니에요)와 긴 문자열을 구분하지 않아요. 둘 다 하나의 항목이고, 작은따옴표 ' '와 큰따옴표 " "를 아무렇게나 바꿔 써도 돼요.

스타일 가이드에서는 큰따옴표를 쓰기를 권장해요. 단, 문자열 안에 큰따옴표를 이스케이프하지 않고 쓰고 싶을 때는 예외예요.

str2 <- 'String with "quotes"'  # convenience format
str2
#> [1] "String with \"quotes\""    # standard format

따라서 character vector는 문자열의 벡터이고, 각 문자열이 하나의 원소예요.

s <- c("some", "strings")
s
#> [1] "some"    "strings"
length(s)
#> [1] 2
length(s[1])  # the number of vector elements, not the string length
#> [1] 1

nchar(s[1])  # the number of characters in the string "some"
#> [1] 4

위 예시에서 length(s[1])은 length("some")과 같아요. 즉 _문자열 길이_가 아니라 _벡터 원소의 개수_예요.

개별 문자열을 조작하는 것도 당연히 가능해요. 위 예시의 nchar()처럼요. 하지만 아래에서 다룰 특별한 함수들이 필요해요.

문자열 함수

base R에서 못 하는 일은 거의 없지만, 많은 프로그래머는 문자열 조작 지원이 다소 제한적이고 헷갈린다고 느껴요.

다행히 훨씬 직관적이고 유연하며 스스로 일관성도 잘 지키는 tidyverse 패키지들이 있어요.

Base R

예전 방식의 문자열 함수를 쓰는 레거시 코드가 여전히 많아요. Exercism의 커뮤니티 풀이 대부분도 여기에 해당해요.

게다가 작은 작업에서는 큰 라이브러리를 불러오는 대신 내장 함수를 쓰는 게 유용할 때가 있어요. R 트랙 관리자들은 연습 문제를 검증할 모범 풀이를 작성할 때 이 방식을 주로 써요.

그래서 예전 방식에 대한 지식도 어느 정도 알아 둘 가치가 있어요.

nchar

문자열의 문자 개수를 반환해요.

nchar("string")
#> [1] 6

nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7

"문자"가 무엇을 뜻하는지는 (복잡해요!) 이 글의 범위를 벗어나요. 위 예시에서 몇 가지만 짚어볼까요:

  • 내부적으로는 C로 구현되어 있지만, R의 문자열은 \0로 끝나지 않아요(그랬다면 길이에 1이 더해졌겠죠).
  • 여러 바이트로 이루어진 유니코드 문자도 예상하거나 바라는 대로 처리돼요. "Hrōðgār"는 7개의 문자(또는 "룬"이나 "코드 포인트"라고도 해요)로 이루어져 있지만, 저장하는 데는 10바이트가 필요해요.
paste와 paste0

임의의 개수만큼의 문자열을 하나로 합쳐요.

paste에는 선택적으로 구분자를 지정할 수 있어요. 기본값은 공백이에요.

paste0(v)는 paste(v, sep = "")와 같아요.

paste("bits", "of", "a", "string")
#> [1] "bits of a string"
paste0("bits", "of", "a", "string")
#> [1] "bitsofastring"
sprintf

문자열 조립을 더 세밀하게 제어하기 위해, R은 C에서 sprintf 함수를 그대로 가져왔어요(이후의 여러 언어들도 마찬가지고요. 위키백과에는 약 30개의 언어가 나열되어 있어요).

r <- 5.3
pi <- 3.14159
sprintf("A circle of radius %.1f has area %.2f", r, pi * r^2)
#> [1] "A circle of radius 5.3 has area 88.25"
trimws

앞쪽이나 뒤쪽의 공백은 trimws()로 제거할 수 있어요:

s <- "    messy string   "
trimws(s)
#> [1] "messy string"

양쪽 끝을 모두 다듬지 않으려면 which = "left"(또는 "right") 매개변수를 써요.

substr

하나의 문자열에 작용해서 두 지점(양 끝 포함) 사이의 부분 문자열을 반환해요.

substr("Exercism", 5, 7)
#> [1] "cis"
strsplit

구분자 문자열을 기준으로 문자열을 R 리스트로 쪼개요. 구분자는 필수예요(기본값이 없어요). 하지만 빈 문자열도 될 수 있어요.

strsplit("R, Python, Julia", ", ")
[[1]]
#> [1] "R"      "Python" "Julia" 

strsplit("Exercism", "")
[[1]]
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"

리스트는 별도의 개념에서 다뤄요. 아직 교육 과정에서 그 부분에 이르지 않았다면, 지금은 unlist() 함수가 (이 경우에는) 리스트를 벡터로 바꿔 준다는 점만 알아두면 돼요.

unlist(strsplit("Exercism", ""))
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"

Tidyverse

좋게 말하자면, 예전 방식의 문자열 함수가 R이 줄 수 있는 최선은 아니에요(더 박하게 보는 시각도 있어요).

이런 상황을 개선하기 위해 stringr 라이브러리는 2009년부터 개발되어 왔고, 지금은 Tidyverse 모음의 핵심 부분이에요.

library(stringr)나 library(tidyverse)로 네임스페이스에 불러올 수 있어요.

주요 특징은 다음과 같아요:

  • 모든 함수 이름에 str_ 접두사가 붙어 있고, 이름만 봐도 목적이 분명하도록 하려고 해요.
  • 인자 순서가 스스로 일관성을 유지하려고 하며, 입력 데이터가 첫 번째 인자예요.
  • 입력은 하나의 문자열부터 복잡한 데이터 구조까지 다양할 수 있어요. 함수는 상황에 맞게 적응해서 가장 합리적인 일을 하려고 해요.

모든 Tidyverse 패키지처럼 stringr도 문서가 잘 갖춰져 있고, 보통 패키지 작성자가 직접 써요.

잠깐 다른 길로: 파이프 빠르게 훑어보기

함수를 연결하는 파이프 |>의 사용법은 교육 과정의 다른 부분에서 더 자세히 다뤄요.

지금은 다음 정도만 알아두면 유용해요:

  • 파이프 왼쪽의 결과가 오른쪽 함수의 첫 번째 인자가 돼요(여기서 "오른쪽"은 선택적인 줄 바꿈은 무시해요).
  • 나머지 인자는 함수 호출에 함께 넣을 수 있어요.
  • R에서는 함수 _이름_이 아니라 함수 _호출_로 파이프해야 하므로, 괄호가 항상 필요해요(다른 몇몇 언어와는 다른 점이에요).

아래 예시는 이 문법을 보여줘요. 개별 함수는 나중에 다뤄요.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
길이 다루기

str_length는 코드 포인트를 세요(nchars와 비슷해요).

str_trim()과 str_squish()를 비롯해 공백을 다루는 함수도 여러 가지가 있어요. 이는 데이터 과학 프로젝트를 시작할 때 꼭 필요한 정리 작업인 경우가 많아요.

# The first 3 kernels in JuPyteR notebooks:
str_length(c("Julia", "Python", "R"))           #> [1] 1 6 5
#> [1] 5 6 1
str_pad(c("Julia", "Python", "R"), 8, "right")  # add spaces if necessary
#> [1] "Julia   " "Python  " "R       "

str_trim("Julia    Python    R    ")   # remove leading & trailing whitespace
#> [1] "Julia    Python    R"
str_squish("Julia   Python  R    ")  # trim, collapse multiple spaces 
#> [1] "Julia Python R"
문자열 일부 추출하기

str_sub(string, start, end) 함수는 start부터 end까지(양 끝 포함)의 부분 문자열을 반환하고, str_sub_all()은 여러 문자열에 한꺼번에 작용할 수 있어요.

유연성이 아주 커요. start와 end는 각각 첫 번째 문자와 마지막 문자를 기본값으로 해요. 인덱스 벡터가 될 수도 있어요. 반가운 점은, 벡터 인덱싱과 달리 음수 값은 문자열 끝에서부터 거꾸로 세요. Python 방식이죠.

s <- "abcdefgh"
str_sub(s, 2, 4)
#> [1] "bcd"
str_sub(s, 5)
#> [1] "efgh"
str_sub(s, -3, -2)
#> [1] "fg"
str_sub(s, c(1, 4), c(2, 5))
#> [1] "ab" "de"
str_sub(s, c(1, 4, 6), c(2, 5, -1))
#> [1] "ab"  "de"  "fgh"

s |> str_sub(2, 4) # these functions are designed for pipes
#> [1] "bcd"
문자열 합치기

str_flatten은 character 벡터를 하나의 문자열로 바꿔요. collapse 인자는 각 원소 사이에 들어가고, 기본값은 ""예요.

c("R", "Julia", "Python") |> str_flatten()
#> [1] "RJuliaPython"
c("R", "Julia", "Python") |> str_flatten(collapse = " - ")
#> [1] "R - Julia - Python"

str_c는 여러 character 벡터를 하나의 character 벡터로 합쳐요. 이때 필요하면 재활용도 해요.

str_c(LETTERS[1:8], 1:8, sep = ":")
#> [1] "A:1" "B:2" "C:3" "D:4" "E:5" "F:6" "G:7" "H:8"

str_glue는 간단한 문자열 보간을 R에 가져와요. 중괄호 { } 안의 표현식이 해석되고, 그 텍스트 표현이 대신 들어가요.

r <- 5.3
pi <- 3.14159
str_glue("A circle of radius {r} has area {pi * r^2}")
A circle of radius 5.3 has area 88.2472631
문자열 나누기

R을 쓸 때 아주 흔한 작업은 지저분한 데이터를 분석하기 좋게 일관된 형식으로 다듬는 거예요. 이를 돕기 위해 텍스트를 부분 문자열로 나누는 여러 방법이 있어요. 대부분은 지금 다룰 범위를 벗어나요.

str_split_1이 가장 간단한데, 하나의 문자열을 지정한 패턴을 기준으로 나눠 조각들의 벡터로 바꿔요(기본 패턴은 없어요).

s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R"      "Julia"  "Python"
문자열 변형하기

일부 언어(주로 유럽 언어)는 대문자와 소문자를 구분해요. 이런 문자를 로케일에 맞게 서로 바꿔 주는 stringr 함수들이 있어요:

  • str_to_lower()
  • str_to_upper()
  • str_to_title() : 각 단어의 첫 글자를 대문자로
  • str_to_sentence() : 문장의 첫 글자를 대문자로
str_to_upper("elixir")    # defaults to locale = "en"
#> [1] "ELIXIR"
str_to_upper("ελληνικά")  # infers Greek
#> [1] "ΕΛΛΗΝΙΚΆ"

str_to_title("the cat sat on the mat")
#> [1] "The Cat Sat On The Mat"
str_to_sentence("the cat sat on the mat")
#> [1] "The cat sat on the mat"

패턴 매칭을 이용한 부분 문자열 교체에는 str_replace()가 패턴이 처음 나타난 곳을 바꾸고, str_replace_all()은 나타나는 모든 곳을 바꿔요.

문자 인덱스를 이용한 부분 문자열 교체에는 str_sub()의 할당 형식이 있어요.

 s <- "abcde"
str_sub(s, 3, 4) <- "yz"
s
#> [1] "abyze"

지침

이 연습 문제에서는 로그 줄을 처리해요.

각 로그 줄은 다음과 같은 형식의 문자열이에요: "[<LEVEL>]: <MESSAGE>".

로그 레벨은 세 가지가 있어요:

  • INFO
  • WARNING
  • ERROR

과제는 세 가지이고, 각 과제는 로그 줄 하나를 받아 그걸로 무언가를 해 보라는 내용이에요.

base R의 레거시 함수를 사용해도 되고 stringr 라이브러리를 사용해도 돼요. 둘 다 짧으면서도 쓸 만한 해법을 만들어 줘요.

1. 로그 줄에서 메시지 가져오기

로그 줄의 메시지를 반환하도록 message 함수를 구현해요:

message("[ERROR]: Invalid operation")
#> "Invalid operation"

앞이나 뒤에 있는 공백은 제거해야 해요:

message("[WARNING]:  Disk almost full\r\n")
#> "Disk almost full"

2. 로그 줄에서 로그 레벨 가져오기

로그 줄의 로그 레벨을 소문자로 반환하도록 log_level 함수를 구현해요:

log_level("[ERROR]: Invalid operation")
#> "error"

3. 로그 줄 형식 바꾸기

메시지를 앞에 두고 그 뒤에 로그 레벨을 괄호 안에 넣도록 로그 줄 형식을 바꾸는 reformat 함수를 구현해요:

reformat("[INFO]: Operation completed")
#> "Operation completed (info)"
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
R Exercism

로그 레벨 문제를 시작해 볼 준비가 됐나요?

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