콘텐츠로 이동

12. 문자열 함수

12.1 기본 처리

데이터를 다루다 보면 여러 조각으로 나뉜 문자값을 하나로 합치거나, 반대로 하나의 문자열에서 필요한 부분만 잘라내야 하는 상황이 자주 발생합니다. 예를 들어 성과 이름을 합쳐 전체 이름을 만들거나, 사용자가 입력한 텍스트 앞뒤의 불필요한 공백을 제거해야 하는 경우입니다.

성·이름을 각각 벡터로 갖고 있다가, 원하는 형태로 합치는 상황을 살펴봅시다.

first <- c("민준", "서연", "도윤")
last  <- c("김", "이", "박")

paste(last, first)              # 공백으로 연결(기본값)
#> [1] "김 민준" "이 서연" "박 도윤"

paste0(last, first)             # 구분자 없이 연결
#> [1] "김민준" "이서연" "박도윤"

paste(last, first, sep = "·")   # 구분자를 "·"로 지정
#> [1] "김·민준" "이·서연" "박·도윤"

paste(first, collapse = ", ")   # 벡터 전체를 하나의 문자열로 축약
#> [1] "민준, 서연, 도윤"

R에서 문자열을 다루는 기본 함수는 크게 세 갈래로 나뉩니다. 여러 값을 하나로 합치는 함수(paste(), paste0(), strrep()), 문자열의 일부를 잘라내거나 나누는 함수(substr(), substring(), strsplit()), 그리고 문자열의 형태(대소문자·공백·정렬)를 다듬는 함수(toupper(), tolower(), trimws(), sprintf(), startsWith(), endsWith())입니다. nchar()는 이 모든 작업의 바탕이 되는 "문자열의 길이"를 알려 줍니다.

이 절에서 다루는 함수들은 공통적으로 문자형 벡터(또는 문자로 강제 변환 가능한 값)를 입력으로 받아 문자형 벡터를 반환합니다. R의 벡터 연산 원칙에 따라 대부분 원소 단위로 동작하며, 길이가 다른 벡터를 함께 넣으면 재활용 규칙(recycling)이 적용됩니다.

paste() / paste0()

paste(..., sep = " ", collapse = NULL)는 여러 값을 문자열로 이어 붙입니다. paste0(..., collapse = NULL)은 paste(..., sep = "")의 축약형으로, 구분자 없이 이어 붙일 때 자주 사용합니다.

  • ... : 이어 붙일 값(문자·숫자 등). 벡터를 넣으면 원소별로 재활용 규칙에 따라 짝지어집니다.
  • sep : 각 인자 사이에 넣을 구분자(기본값은 공백 하나). paste0()에는 이 인자가 없습니다(항상 빈 문자열).
  • collapse : 결과로 나온 벡터의 모든 원소를 다시 하나의 문자열로 합칠 때 사용할 구분자. NULL(기본값)이면 합치지 않고 벡터 그대로 반환합니다.

기본 예제는 위 "구체적 사례"에서 다루었습니다. 숫자처럼 길이가 서로 다른 값을 함께 넣으면 짧은 쪽이 재활용됩니다.

paste("항목", 1:5)
#> [1] "항목 1" "항목 2" "항목 3" "항목 4" "항목 5"

paste0("Q", 1:4, "_", c("결과", "점수"))
#> [1] "Q1_결과" "Q2_점수" "Q3_결과" "Q4_점수"

nchar()

nchar(x, type = c("chars", "bytes", "width"), keepNA = NA)는 문자열의 길이를 셉니다.

  • x : 길이를 셀 문자형 벡터(문자가 아니면 내부적으로 문자로 변환됩니다).
  • type : 무엇을 기준으로 "길이"를 셀지 지정합니다. "chars"(기본값)는 문자(글자) 개수, "bytes"는 저장에 사용된 바이트 수, "width"는 화면에 표시될 때 차지하는 폭(한글·한자 등 동아시아 문자는 폭 2로 계산)입니다.
  • keepNA : NA 원소를 결과에서도 NA로 유지할지 정합니다. 기본값 NA는 type = "width"일 때만 FALSE처럼(즉 "NA"라는 두 글자로) 동작하고, 그 외에는 TRUE처럼(즉 결과도 NA로) 동작합니다.

영문과 달리 한글은 한 글자가 여러 바이트로 저장되기 때문에, type을 어떻게 지정하느냐에 따라 결과가 크게 달라집니다.

nchar("R 언어")                  # 문자 개수
#> [1] 4

nchar("R 언어", type = "bytes")  # UTF-8 기준 바이트 수(한글 1자 = 3바이트)
#> [1] 8

nchar(c("사과", "바나나", NA))
#> [1]  2  3 NA

type = "width"는 콘솔에 출력했을 때 실제로 차지하는 자리 수를 알려 주므로, 결과를 표처럼 정렬해서 보여줄 때 참고할 수 있습니다.

x <- c("apple", "사과")
nchar(x, type = "chars")   # 문자 개수: 5, 2
#> [1] 5 2

nchar(x, type = "bytes")   # 바이트 수: 5, 6 (한글 2자 = 6바이트, UTF-8 기준)
#> [1] 5 6

nchar(x, type = "width")   # 표시 폭: 5, 4 (한글 1자 = 폭 2)
#> [1] 5 4

💡 이 표시 폭 차이는 뒤에서 다룰 sprintf()의 정렬(%-10s 등)에서도 그대로 나타납니다. sprintf()의 폭 지정은 문자 개수가 아니라 바이트 수를 기준으로 계산되므로, 한글이 섞인 문자열을 정렬할 때는 예상과 다른 결과가 나올 수 있습니다(아래 sprintf() 절의 주의 참고).

toupper() / tolower()

toupper(x)와 tolower(x)는 문자열의 영문 알파벳을 각각 대문자·소문자로 바꿉니다. 한글이나 숫자처럼 대소문자 구분이 없는 문자는 그대로 유지됩니다.

  • x : 변환할 문자형 벡터.
toupper("Data Analysis")
#> [1] "DATA ANALYSIS"

tolower("DATA ANALYSIS")
#> [1] "data analysis"

substr() / substring()

substr(x, start, stop)는 문자열 x에서 start~stop 위치에 해당하는 부분을 잘라냅니다. substring(text, first, last = 1000000L)은 substr()과 거의 같은 일을 하지만, first·last에 벡터를 넣으면 하나의 문자열에서 여러 구간을 한 번에 잘라낼 수 있다는 차이가 있습니다.

  • x, text : 대상 문자열.
  • start, stop / first, last : 잘라낼 구간의 시작·끝 위치(1부터 시작). stop이 문자열 길이를 넘으면 끝까지만 잘라내고 오류가 나지 않습니다.
x <- "R Basic Manual"
substr(x, 1, 5)            # 1~5번째 글자
#> [1] "R Bas"

substr(x, 3, 100)          # 끝(stop)이 문자열 길이를 넘어도 안전하게 끝까지만 반환
#> [1] "Basic Manual"

substring(x, c(1, 3, 5))   # 시작 위치를 여러 개 지정 -> 여러 구간을 한 번에
#> [1] "R Basic Manual" "Basic Manual"   "sic Manual"    

substr()은 <-(할당 연산자)와 함께 쓰면 문자열의 일부를 다른 값으로 바꿔치기할 수도 있습니다.

y <- "Hello World"
substr(y, 1, 5) <- "HELLO"
y
#> [1] "HELLO World"

strsplit()

strsplit(x, split, fixed = FALSE, perl = FALSE)는 문자열 x를 split을 기준으로 나누어 리스트로 반환합니다.

  • x : 나눌 문자형 벡터.
  • split : 구분자. 기본적으로 정규표현식(12.3절)으로 해석되며, 문자 그대로 취급하려면 fixed = TRUE를 지정합니다.
  • fixed, perl : sub()·gsub()(12.2절)과 동일한 역할입니다.

x가 길이 2 이상인 벡터면, 원소마다 나뉜 결과가 리스트의 각 원소로 담깁니다.

strsplit("2026-07-21", "-")
#> [[1]]
#> [1] "2026" "07"   "21"  

strsplit(c("a,b,c", "d,e"), ",")
#> [[1]]
#> [1] "a" "b" "c"
#> 
#> [[2]]
#> [1] "d" "e"

리스트로 반환되기 때문에, 나뉜 결과에 다시 함수를 적용하려면 8장에서 다룬 sapply()·lapply()와 함께 쓰는 경우가 많습니다.

sapply(strsplit(c("a,b,c", "d,e"), ","), length)   # 나뉜 조각의 개수
#> [1] 3 2

trimws()

trimws(x, which = c("both", "left", "right"), whitespace = "[ \t\r\n]")는 문자열 양 끝의 공백 문자를 제거합니다(R 3.2 이상).

  • x : 대상 문자형 벡터.
  • which : 제거할 방향. "both"(기본값)는 양쪽, "left"는 왼쪽만, "right"는 오른쪽만 제거합니다.
  • whitespace : 공백으로 취급할 문자를 정규표현식으로 지정합니다(기본값은 스페이스·탭·캐리지리턴·줄바꿈).

설문 응답이나 외부 파일에서 읽어온 텍스트에는 의도치 않은 앞뒤 공백이 섞여 있는 경우가 많아, 데이터 정제 시 가장 먼저 적용하게 되는 함수 중 하나입니다.

trimws("   여백 제거   ")
#> [1] "여백 제거"

trimws("   왼쪽만   ", which = "left")
#> [1] "왼쪽만   "

sprintf()

sprintf(fmt, ...)는 C 언어의 printf 계열 함수와 동일한 방식으로, 서식 문자열 fmt에 맞추어 값을 채워 넣은 문자열을 만듭니다.

  • fmt : 서식 문자열. %d(정수), %f(실수), %s(문자열), %.Nf(소수점 N자리), %0Nd(N자리, 빈 자리를 0으로 채움), %-Ns(왼쪽 정렬, 폭 N)처럼 %로 시작하는 변환 지정자를 포함합니다.
  • ... : fmt의 변환 지정자 개수만큼 채워 넣을 값들. 벡터를 넣으면 원소별로 재활용 규칙에 따라 반복됩니다.

paste()가 값을 단순히 이어 붙이는 데 집중한다면, sprintf()는 소수점 자릿수·자릿수 맞춤·정렬처럼 출력 형식을 세밀하게 지정해야 할 때(보고서·로그 메시지 등) 유용합니다.

sprintf("%d개 중 %d개 완료 (%.1f%%)", 10, 7, 70)
#> [1] "10개 중 7개 완료 (70.0%)"

sprintf("%05d", 42)               # 5자리, 빈 자리는 0으로
#> [1] "00042"

sprintf("%-10s|", "왼쪽정렬")      # 왼쪽 정렬, 폭 10
#> [1] "왼쪽정렬|"

벡터를 넣으면 원소마다 서식이 적용되므로, 데이터프레임의 여러 행에 대해 보고서용 문장을 한 번에 생성할 때 편리합니다.

name  <- c("김민준", "이서연")
score <- c(88.456, 92.1)
sprintf("%s: %.1f점", name, score)
#> [1] "김민준: 88.5점" "이서연: 92.1점"

주의: 폭 지정(%Ns)은 문자 개수가 아니라 바이트 수 기준입니다. 위 "왼쪽정렬|" 예제에서 %-10s를 지정했는데도 "왼쪽정렬"(4글자) 뒤에 공백이 하나도 붙지 않은 것을 볼 수 있습니다. "왼쪽정렬"은 UTF-8로 12바이트이기 때문에, 이미 지정한 폭(10)을 넘어서 버려 패딩이 전혀 들어가지 않은 것입니다. 반면 2글자(6바이트)인 문자열은 폭 10을 채우기 위해 공백 4개가 그대로 붙습니다.

nchar(sprintf("%-10s|", "가나"))   # "가나"(6바이트) + 공백 4개 + "|" = 7글자
#> [1] 7

한글이 섞인 문자열을 표처럼 정렬해서 출력해야 한다면, 이처럼 글자 수와 바이트 수의 차이 때문에 정렬이 어긋날 수 있다는 점을 감안해야 합니다.

startsWith() / endsWith()

startsWith(x, prefix)와 endsWith(x, suffix)는 문자열 x가 특정 접두사(prefix)·접미사(suffix)로 시작·끝나는지 검사합니다(R 3.3 이상).

  • x : 검사할 문자형 벡터.
  • prefix, suffix : 검사할 접두사·접미사(길이 1 또는 x와 같은 길이의 벡터).

grepl()(12.2절)로 "^report"·"\\.csv$"처럼 정규표현식을 써서 같은 검사를 할 수도 있지만, 정규식 해석 과정이 없는 만큼 더 빠르고 의도도 분명합니다.

files <- c("report_final.docx", "report_draft.docx", "data_2026.csv")
startsWith(files, "report")
#> [1]  TRUE  TRUE FALSE

endsWith(files, ".csv")
#> [1] FALSE FALSE  TRUE

files[endsWith(files, ".csv")]
#> [1] "data_2026.csv"

strrep()

strrep(x, times)는 문자열 x를 times번 반복해서 이어 붙입니다.

  • x : 반복할 문자형 벡터.
  • times : 반복 횟수(x와 길이가 다르면 재활용 규칙이 적용됩니다).

콘솔에 구분선을 출력하거나 들여쓰기 수준에 따라 공백을 반복해서 만들 때, paste(rep(x, n), collapse = "")보다 간결합니다.

strrep("-", 20)
#> [1] "--------------------"

strrep(c("A", "B"), times = c(3, 2))
#> [1] "AAA" "BB" 

더 알아보기: chartr() — 문자 단위 치환

chartr(old, new, x)는 문자열 x에서 old에 있는 각 문자를 같은 위치의 new 문자로 하나씩 바꿉니다. 패턴이 아니라 문자 하나하나를 1:1로 대응시켜 바꾼다는 점에서, 문자열(패턴) 단위로 치환하는 gsub()(12.2절)과 다릅니다.

chartr("abc", "xyz", "aabbcc")   # a->x, b->y, c->z
#> [1] "xxyyzz"

12.2 패턴 매칭

여러 문자열 중에서 특정 패턴을 포함한 것만 골라내거나, 잘못 입력된 값을 올바른 값으로 일괄 치환해야 하는 경우가 많습니다. 예를 들어 이메일 주소 목록에서 특정 도메인만 찾아내거나, 개인정보가 담긴 전화번호의 가운데 자리를 마스킹 처리하는 상황입니다.

구체적 사례:

emails <- c("kim@naver.com", "lee@gmail.com", "park@naver.com", "choi@hanmail.net")

grepl("naver", emails)                       # 패턴 포함 여부(논리값)
#> [1]  TRUE FALSE  TRUE FALSE

grep("naver", emails)                        # 패턴을 포함한 원소의 위치(인덱스)
#> [1] 1 3

grep("naver", emails, value = TRUE)          # 패턴을 포함한 원소 자체
#> [1] "kim@naver.com"  "park@naver.com"

gsub("naver.com", "kakao.com", emails)       # 패턴을 다른 문자열로 치환
#> [1] "kim@kakao.com"    "lee@gmail.com"    "park@kakao.com"   "choi@hanmail.net"

이 절의 함수들은 "찾기"와 "바꾸기" 두 갈래로 나뉩니다. grep()·grepl()은 패턴을 포함한 원소를 찾아 위치·논리값·값 자체로 돌려주고, sub()·gsub()은 찾은 패턴을 다른 문자열로 바꿉니다. match()·pmatch()·charmatch()는 이와 성격이 조금 달라서, 패턴 검색이 아니라 x의 각 원소가 참조 벡터(table)의 어떤 원소와 정확히(또는 부분적으로) 일치하는지를 찾습니다.

grep() / grepl()

grep(pattern, x, ignore.case = FALSE, value = FALSE, fixed = FALSE, ...)는 문자형 벡터 x의 원소 중 pattern을 포함하는 원소의 위치(또는 value = TRUE이면 값 자체)를 반환합니다. grepl()은 같은 검사를 하되 x와 같은 길이의 논리값 벡터로 반환합니다("grep logical"의 줄임).

  • pattern : 찾을 패턴(정규표현식).
  • x : 검사할 문자형 벡터.
  • ignore.case : TRUE이면 대소문자를 구분하지 않습니다.
  • value : TRUE이면 위치 대신 매칭된 원소 자체를 반환합니다(grep()만 해당).
  • fixed : TRUE이면 pattern을 정규표현식이 아니라 문자 그대로 취급합니다.
words <- c("Apple", "banana", "APPLE", "Cherry")
grepl("apple", words)                       # 대소문자를 구분하므로 모두 FALSE
#> [1] FALSE FALSE FALSE FALSE

grepl("apple", words, ignore.case = TRUE)   # 대소문자 무시
#> [1]  TRUE FALSE  TRUE FALSE

if() 조건이나 subset()의 조건식에 grepl()을 그대로 활용하는 경우가 많은데, 이는 논리값 벡터를 돌려주는 grepl()의 특징 덕분입니다.

sub() / gsub()

sub(pattern, replacement, x, ...)는 x의 각 원소에서 pattern과 처음 일치하는 부분만 replacement로 바꿉니다. gsub()("global sub")는 일치하는 모든 부분을 바꾼다는 점만 다릅니다.

  • pattern, x, fixed : grep()과 동일합니다.
  • replacement : 바꿔 넣을 문자열. 정규표현식의 캡처 그룹(소괄호로 묶은 부분)을 \\1, \\2처럼 참조해 원본의 일부를 그대로 살려 쓸 수 있습니다(12.3절 regexec()와 연결).
x <- "aaa-bbb-ccc"
sub("-", "_", x)    # 첫 번째 "-"만 치환
#> [1] "aaa_bbb-ccc"

gsub("-", "_", x)   # 모든 "-"를 치환
#> [1] "aaa_bbb_ccc"

캡처 그룹을 활용하면 패턴의 일부는 유지하면서 특정 부분만 바꾸는 정교한 치환도 가능합니다. 예를 들어 전화번호의 가운데 네 자리만 마스킹하는 경우입니다.

phones <- c("010-1234-5678", "010-9876-5432")
gsub("(\\d{3})-(\\d{4})-(\\d{4})", "\\1-****-\\3", phones)
#> [1] "010-****-5678" "010-****-5432"

match() / %in%

match(x, table, nomatch = NA_integer_)는 x의 각 원소가 table의 몇 번째 원소와 정확히 일치하는지 그 위치를 반환합니다. x %in% table은 match()를 이용해 구현된 연산자로, 위치 대신 일치 여부만 논리값으로 간단히 얻고 싶을 때 씁니다.

  • x : 찾을 값들.
  • table : 검색 대상이 되는 값들.
  • nomatch : table에서 찾지 못했을 때 대신 채울 값(기본값 NA).
grades <- c("B", "A", "F", "C")
match(grades, c("A", "B", "C", "D", "F"))   # 각 값이 table의 몇 번째인지
#> [1] 2 1 5 3

grades %in% c("A", "B")                      # A 또는 B인지만 확인
#> [1]  TRUE  TRUE FALSE FALSE

match("E", c("A", "B", "C", "D", "F"))       # 없는 값은 NA
#> [1] NA

pmatch() / charmatch()

pmatch(x, table, nomatch = NA_integer_, duplicates.ok = FALSE)는 x의 각 값이 table의 어떤 값의 앞부분과 일치하는지 찾는 부분 매칭(partial matching)을 수행합니다. charmatch(x, table, nomatch = NA_integer_)도 목적은 같지만, 일치하는 후보가 여럿이라 애매할 때의 처리 방식이 다릅니다.

  • x : 찾을 값(보통 줄여 쓴 이름).
  • table : 검색 대상이 되는 전체 이름들.
  • nomatch : 일치하는 후보가 전혀 없을 때 채울 값.
  • duplicates.ok(pmatch()만 해당) : FALSE(기본값)이면 table의 한 원소가 x의 여러 값과 중복으로 매칭되는 것을 허용하지 않습니다.

함수의 인자 이름을 줄여 써도 인식하는 R의 부분 매칭 규칙(16.2절)이 바로 이 원리로 동작합니다.

pmatch("me", c("mean", "median"))      # "mean"·"median" 둘 다에 해당 -> 애매 -> NA
#> [1] NA

pmatch("mea", c("mean", "median"))     # "mean"에만 해당 -> 고유하게 매칭
#> [1] 1

pmatch("median", c("mean", "median"))  # 완전히 일치하는 값이 있으면 그 위치
#> [1] 2

charmatch()는 애매한 경우를 NA가 아니라 0으로 구분해서 알려 줍니다. 즉 반환값이 0이면 "일치 후보가 여럿", NA이면 "일치 후보가 아예 없음"이라는 뜻입니다.

charmatch("m", c("mean", "median"))    # 두 후보 모두 해당 -> 0(애매)
#> [1] 0

charmatch("me", c("mean", "median"))   # 역시 두 후보 모두 해당 -> 0(애매)
#> [1] 0

charmatch("z", c("mean", "median"))    # 후보 없음 -> NA
#> [1] NA

더 알아보기: agrep() — 근사(퍼지) 문자열 매칭

agrep(pattern, x, max.distance = 0.1, ...)는 오타나 철자 차이가 있어도 비슷한 문자열을 찾아 주는 근사 매칭(approximate/fuzzy matching) 함수입니다. max.distance는 허용할 편집 거리(edit distance, 한 문자를 삽입·삭제·치환하는 데 필요한 최소 횟수)로, 0~1 사이의 비율 또는 정수 횟수로 지정할 수 있습니다.

agrep("Lvine", c("Levine", "Lavine", "Irvine"), max.distance = 1)
agrep("Lvine", c("Levine", "Lavine", "Irvine"), max.distance = 1, value = TRUE)
#> [1] 1 2 3
#> [1] "Levine" "Lavine" "Irvine"

사람이 직접 입력한 응답(설문·회원명부 등)에서 같은 대상을 가리키지만 표기가 조금씩 다른 값을 하나로 묶어야 할 때 유용합니다. 다만 max.distance를 너무 느슨하게 잡으면 관계없는 값까지 걸릴 수 있으므로, 결과는 반드시 눈으로 확인해야 합니다.

12.3 정규표현식

grep()·sub()은 "패턴을 포함하는지" 확인하거나 "찾은 부분을 다른 값으로 바꾸는" 데는 유용하지만, "정확히 어떤 부분이 매칭되었는지" 그 자체를 알아내거나, 패턴 안에서 여러 조각(그룹)으로 나누어 각각 따로 추출해야 하는 경우에는 한계가 있습니다.

주문 내역 문자열에서 숫자로 된 부분만 모두 추출하는 상황을 생각해 봅니다.

x <- "주문번호 A1234, 수량 7개, 가격 15000원"

regexpr("[0-9]+", x)                    # 첫 번째로 일치하는 부분의 위치·길이
#> [1] 7
#> attr(,"match.length")
#> [1] 4

regmatches(x, regexpr("[0-9]+", x))     # 그 부분의 실제 값
#> [1] "1234"

gregexpr("[0-9]+", x)                   # 일치하는 모든 부분의 위치·길이
#> [[1]]
#> [1]  7 16 23
#> attr(,"match.length")
#> [1] 4 1 5

regmatches(x, gregexpr("[0-9]+", x))    # 모든 부분의 실제 값
#> [[1]]
#> [1] "1234"  "7"     "15000"

regexpr()·gregexpr()은 패턴이 어디서(위치) 몇 글자만큼(길이) 일치하는지를 찾아내는 "탐지" 함수이고, regmatches()는 그 탐지 결과를 받아 실제 문자열 값으로 "추출"(또는 치환)하는 함수입니다. regexec()는 여기에 더해 정규표현식의 캡처 그룹(소괄호로 묶은 부분)별로 위치를 따로 찾아 준다는 점에서 한 단계 더 정교합니다.

regexpr() / gregexpr()

regexpr(pattern, text, ignore.case = FALSE, fixed = FALSE, perl = FALSE, ...)는 text의 각 원소에서 pattern과 처음 일치하는 부분의 시작 위치를 반환하며, 일치한 부분의 길이는 match.length라는 속성(attribute)에 함께 담아 돌려줍니다. gregexpr()("global regexpr")는 처음 하나가 아니라 일치하는 모든 부분의 위치·길이를 리스트로 반환합니다.

  • pattern : 찾을 정규표현식.
  • text : 검사할 문자형 벡터.
  • ignore.case, fixed : grep()과 동일합니다.
  • perl : TRUE이면 PCRE(Perl 호환) 정규식 엔진을 사용해, 전방탐색(lookahead)·후방탐색(lookbehind) 같은 고급 문법을 쓸 수 있습니다.

일치하는 부분이 없으면 위치·길이 모두 -1을 반환합니다.

regexpr("zzz", "abc")   # 일치하는 부분이 없음
#> [1] -1
#> attr(,"match.length")
#> [1] -1
#> attr(,"index.type")
#> [1] "chars"
#> attr(,"useBytes")
#> [1] TRUE

반환값 자체는 정수 벡터처럼 보이지만 match.length 속성이 함께 붙어 있다는 점이 이 함수들의 핵심입니다. 이 속성 덕분에 아래에서 다룰 regmatches()가 어디서부터 몇 글자를 잘라내야 하는지 알 수 있습니다.

regmatches()

regmatches(x, m, invert = FALSE)는 regexpr()·gregexpr()·regexec()가 반환한 위치 정보 m을 이용해, x에서 실제로 일치한 부분 문자열을 잘라내 돌려줍니다.

  • x : 원본 문자형 벡터.
  • m : regexpr()류 함수가 반환한, 위치·길이 정보가 담긴 객체.
  • invert : TRUE이면 반대로 일치하지 않은 부분들을 잘라내 반환합니다.

regmatches(x, m) <- value 형태로 왼쪽에 놓으면, 잘라내는 대신 그 자리에 값을 바꿔 넣는 치환으로도 쓸 수 있습니다(sub()의 그룹 치환보다 더 세밀하게 제어하고 싶을 때 유용합니다).

y <- x
regmatches(y, regexpr("[0-9]+", y)) <- "***"
y
#> [1] "주문번호 A***, 수량 7개, 가격 15000원"

regexec()

regexec(pattern, text, ...)는 regexpr()처럼 첫 번째로 일치하는 부분을 찾되, pattern 안에 소괄호로 묶은 캡처 그룹이 있으면 전체 일치 부분뿐 아니라 그룹별 위치까지 함께 반환합니다.

  • pattern, text, ignore.case, fixed, perl : regexpr()과 동일합니다. 다만 캡처 그룹, 즉 ()이 있어야 실질적인 효과가 있습니다.

regmatches()와 짝지어 쓰면, 전체 매칭 값과 함께 그룹별로 나뉜 값들을 한 번에 벡터로 받을 수 있습니다. 날짜 문자열을 연·월·일로 나누어 추출하는 예가 대표적입니다.

date_str <- "2026-07-21"
m <- regexec("([0-9]{4})-([0-9]{2})-([0-9]{2})", date_str)
regmatches(date_str, m)
#> [[1]]
#> [1] "2026-07-21" "2026"       "07"         "21"        

결과 리스트의 첫 번째 값은 패턴 전체와 일치한 문자열이고, 이어지는 값들은 순서대로 첫 번째, 두 번째, 세 번째 캡처 그룹입니다. 이 방식은 여러 개의 문자열에도 그대로 적용할 수 있습니다.

dates <- c("2026-07-21", "2025-12-05")
regmatches(dates, regexec("([0-9]{4})-([0-9]{2})-([0-9]{2})", dates))
#> [[1]]
#> [1] "2026-07-21" "2026"       "07"         "21"        
#> 
#> [[2]]
#> [1] "2025-12-05" "2025"       "12"         "05"        

참고: raw string(원문 문자열) — R 4.0 이상

정규표현식이나 윈도우 파일 경로에는 백슬래시(\)가 자주 등장하는데, 일반 문자열 안에서 백슬래시 하나를 표현하려면 "\\"처럼 두 번 겹쳐 써야 해서(이스케이프) 코드가 읽기 번거로워집니다. R 4.0부터 지원하는 raw string 문법 r"(...)"을 쓰면 괄호 안의 내용을 이스케이프 없이 있는 그대로 문자열로 만들 수 있습니다.

path1 <- "C:\\Users\\admin\\Documents"   # 일반 문자열: 백슬래시를 두 번씩
path2 <- r"(C:\Users\admin\Documents)"    # raw string: 있는 그대로 한 번만
identical(path1, path2)
#> [1] TRUE

다만 raw string은 어디까지나 "R이 문자열 리터럴을 어떻게 읽어 들이는가"의 문제일 뿐, 그렇게 만들어진 문자열이 정규표현식으로 어떻게 해석되는지와는 별개입니다. 백슬래시는 정규식 엔진 입장에서도 이스케이프 문자이므로, 경로처럼 백슬래시를 문자 그대로 찾고 싶다면 raw string과 fixed = TRUE를 함께 쓰는 것이 안전합니다.

grepl(r"(C:\Users)", path2, fixed = TRUE)   # 정규식이 아니라 문자 그대로 검색
#> [1] TRUE

주의: fixed·perl 옵션과 정규식 특수문자

grep()·sub()·strsplit()·regexpr() 계열 함수는 모두 fixed·perl이라는 공통 옵션을 갖습니다. fixed = TRUE는 pattern을 정규표현식이 아니라 문자 그대로 취급하도록 강제하고, perl = TRUE는 PCRE 엔진을 사용해 전방탐색(lookahead)·후방탐색(lookbehind) 같은 고급 문법을 활성화합니다.

grepl(".", "abc", fixed = TRUE)   # "."을 마침표 문자 그대로 찾음 -> 없음
grepl(".", "a.c", fixed = TRUE)   # 마침표가 실제로 있는 경우 -> 있음
#> [1] FALSE
#> [1] TRUE

\d(숫자)·\s(공백)·\w(단어 문자) 같은 축약 표기는 R의 기본 정규식 엔진(TRE)에서도 대부분 그대로 동작하지만, 전방탐색·후방탐색처럼 더 복잡한 문법은 perl = TRUE가 있어야만 인식됩니다. 예를 들어 숫자에 세 자리마다 쉼표를 넣는 다음 코드는 perl = TRUE 없이는 오류가 납니다.

gsub("(?<=[0-9])(?=(?:[0-9]{3})+$)", ",", "1234567", perl = TRUE)
#> [1] "1,234,567"

정규표현식 특수문자 요약

문자열 함수의 pattern 인자에 자주 쓰이는 정규표현식 메타문자를 정리하면 다음과 같습니다. 레퍼런스로 참고하되, 정확한 동작은 항상 작은 예제로 직접 확인하는 것이 안전합니다.

표기 의미 예시
. 임의의 문자 한 개 "a.c"는 "abc", "a1c" 등과 일치
^ 문자열의 시작 "^R"은 "R"로 시작하는 문자열과 일치
$ 문자열의 끝 "csv$"는 "csv"로 끝나는 문자열과 일치
* 앞 패턴이 0회 이상 반복 "ab*"는 "a", "ab", "abb" 등과 일치
+ 앞 패턴이 1회 이상 반복 "[0-9]+"는 하나 이상의 연속된 숫자
? 앞 패턴이 0회 또는 1회 "colou?r"는 "color"·"colour" 모두 일치
{n,m} 앞 패턴이 n~m회 반복 "[0-9]{3,4}"는 3~4자리 숫자
[ ] 대괄호 안의 문자 중 하나 "[aeiou]"는 모음 하나
[^ ] 대괄호 안에 없는 문자 "[^0-9]"는 숫자가 아닌 문자
( ) 그룹 묶기·캡처 sub()의 \\1 등으로 재참조
\| 여러 패턴 중 하나(OR) "cat\|dog"는 "cat" 또는 "dog"
\d \s \w 숫자·공백·단어 문자(축약형) "\\d{4}"는 4자리 숫자
(?=...) (?<=...) 전방·후방탐색(perl = TRUE 필요) 위 세 자리 쉼표 예제 참고

셸(shell)의 *.csv 같은 와일드카드 표기(glob 패턴)에 익숙하다면, glob2rx()로 이를 정규표현식으로 변환해 grep() 등에 바로 사용할 수도 있습니다.

glob2rx("*.csv")
#> [1] "^.*\\.csv$"

grepl(glob2rx("*.csv"), c("data.csv", "data.csv.bak", "x.txt"))
#> [1]  TRUE FALSE FALSE