콘텐츠로 이동

14. 입출력 함수

14.1 파일 읽기

학생들의 성적이 담긴 CSV 파일을 받았다고 하겠습니다. 이 파일을 R로 읽어 데이터프레임으로 만들어야 분석을 시작할 수 있는데, 파일마다 값을 구분하는 기호(쉼표·탭·공백 등)나 소수점 표기, 첫 줄이 변수 이름인지 여부가 제각각이라 하나의 함수만으로는 모든 경우를 처리하기 어렵습니다.

실습에 사용할 파일은 다음 코드로 미리 만들어 둡니다(파일을 만드는 write.csv()는 다음 절에서 자세히 다룹니다).

성적 <- data.frame(
  이름 = c("김민준", "이서연", "박도윤", "최지우", "정하은"),
  국어 = c(88, 95, 76, 90, 82),
  영어 = c(92, 88, 90, 74, 85),
  수학 = c(79, 84, 95, 88, 91)
)
write.csv(성적, "성적.csv", row.names = FALSE)

만들어진 파일의 내용은 다음과 같습니다(readLines()는 이 절 뒤에서 다시 다룹니다).

cat(readLines("성적.csv"), sep = "\n")
#> "이름","국어","영어","수학"
#> "김민준",88,92,79
#> "이서연",95,88,84
#> "박도윤",76,90,95
#> "최지우",90,74,88
#> "정하은",82,85,91

이 파일을 읽어 들여 과목별 평균을 계산하는 것이 이 절과 다음 절(14.2)에서 이어질 실습의 큰 흐름입니다.

성적 <- read.csv("성적.csv")
성적$평균 <- round(rowMeans(성적[2:4]), 1)
성적
#>     이름 국어 영어 수학 평균
#> 1 김민준   88   92   79 86.3
#> 2 이서연   95   88   84 89.0
#> 3 박도윤   76   90   95 87.0
#> 4 최지우   90   74   88 84.0
#> 5 정하은   82   85   91 86.0

이 절의 함수들은 크게 "값이 표(table) 형태로 정리된 파일을 읽는가"와 "구분자 없이 줄(line) 그대로 읽는가"로 나뉩니다. 전자는 read.table()과 그 wrapper 함수들(read.csv() 등), read.fwf(), scan()이 맡고, 후자는 readLines()가 맡습니다. 여기에 R 객체 자체를 그대로 복원하는 readRDS()가 더해집니다.

read.table()

read.table(file, header = FALSE, sep = "", quote = "\"'", dec = ".", na.strings = "NA", colClasses = NA, skip = 0, nrows = -1, check.names = TRUE, stringsAsFactors = FALSE, fileEncoding = "", encoding = "unknown", ...)는 텍스트 파일 file을 읽어 데이터프레임으로 반환하는, 이 절의 가장 기본이 되는 함수입니다.

  • file : 읽어 들일 파일의 경로(문자열) 또는 연결(connection) 객체. "http://"·"https://"로 시작하는 인터넷 주소를 그대로 넣으면 웹에 있는 파일도 곧바로 읽을 수 있습니다(자세한 내용은 아래 "더 알아보기" 참고).
  • header : 파일의 첫 줄을 변수(열) 이름으로 볼지 여부. 기본값은 FALSE이므로, 첫 줄이 데이터로 잘못 섞여 들어가지 않도록 하려면 반드시 TRUE로 지정해야 합니다.
  • sep : 값을 구분하는 구분자. 기본값 ""은 공백·탭 등 하나 이상의 공백류 문자를 구분자로 봅니다. 쉼표로 구분된 파일이라면 sep = ","를 지정해야 합니다.
  • quote : 문자열 값을 감싸는 따옴표로 인식할 문자들. 기본값은 큰따옴표(")와 작은따옴표(') 둘 다입니다.
  • dec : 소수점으로 쓸 기호. 기본값은 마침표(.)이며, 쉼표(,)를 소수점으로 쓰는 자료라면 dec = ","로 바꿔야 합니다.
  • na.strings : 결측치(NA)로 처리할 문자열들. 기본값은 "NA" 하나뿐이므로, 자료에서 결측을 "-"나 빈 칸 등 다른 기호로 표시했다면 이 인자로 직접 지정해야 합니다.
  • colClasses : 각 열의 자료형을 미리 지정합니다. 지정하지 않으면(기본값 NA) R이 각 열의 값을 보고 자동으로 자료형을 추측합니다.
  • skip : 파일 맨 앞에서 건너뛸 줄 수. 파일 위쪽에 안내문 등이 붙어 있을 때 유용합니다.
  • nrows : 최대로 읽어 들일 행 수. 매우 큰 파일에서 앞부분 일부만 미리 살펴보고 싶을 때 지정합니다.
  • check.names : TRUE(기본값)이면 열 이름이 R 변수명 규칙에 맞지 않을 때(공백·특수문자 포함 등) make.names()로 자동 변환합니다.
  • stringsAsFactors : 문자열 열을 팩터(factor)로 변환할지 여부. R 4.0.0부터 기본값이 FALSE로 바뀌어, 이제는 별도로 지정하지 않는 한 문자열이 문자형(character) 그대로 유지됩니다.
  • fileEncoding, encoding : 파일이 R 세션과 다른 문자 인코딩(예: "CP949", "UTF-8")으로 저장되어 있을 때 지정합니다.

앞서 만든 파일은 쉼표(,)로 구분되어 있으므로, read.table()로 읽으려면 sep과 header를 직접 지정해야 합니다.

d1 <- read.table("성적.csv", header = TRUE, sep = ",")
str(d1)
#> 'data.frame':    5 obs. of  4 variables:
#>  $ 이름: chr  "김민준" "이서연" "박도윤" "최지우" ...
#>  $ 국어: int  88 95 76 90 82
#>  $ 영어: int  92 88 90 74 85
#>  $ 수학: int  79 84 95 88 91

이름 열이 chr(문자형)로 표시된 점에 주목해야 합니다. R 4.0 이전이었다면 stringsAsFactors의 기본값이 TRUE여서 이 열이 팩터(Factor)로 바뀌었을 것입니다.

read.csv() / read.csv2() / read.delim() / read.delim2()

read.table()은 모든 옵션을 직접 지정해야 하므로, 자주 쓰이는 파일 형식마다 sep·header·dec의 기본 조합을 미리 정해 둔 네 가지 wrapper 함수가 함께 제공됩니다. 넷 다 내부적으로는 read.table()을 호출할 뿐이며, read.table()의 다른 인자(na.strings, colClasses, fileEncoding 등)도 그대로 추가로 넘길 수 있습니다.

함수 sep 기본값 header 기본값 dec 기본값 주로 쓰는 상황
read.csv() "," TRUE "." 쉼표로 구분된 CSV(한국·영어권 표준)
read.csv2() ";" TRUE "," 세미콜론으로 구분, 소수점에 쉼표를 쓰는 유럽식 CSV
read.delim() "\t"(탭) TRUE "." 탭으로 구분된 파일
read.delim2() "\t"(탭) TRUE "," 탭 구분 + 유럽식 소수점

앞서 read.table()로 지정했던 옵션들이 read.csv()에는 이미 기본값으로 들어 있으므로, 다음 두 코드는 완전히 같은 결과를 냅니다.

d2 <- read.csv("성적.csv")
identical(d1, d2)
#> [1] TRUE

read.csv2()·read.delim2()가 필요한 이유는 "쉼표"의 역할이 나라마다 다르기 때문입니다. 예를 들어 다음처럼 세미콜론으로 구분하고 소수점에 쉼표를 쓰는 파일(eu.csv)이 있다고 하겠습니다.

제품;가격
사과;1500,5
바나나;2300,0
포도;4200,75

주의: 이런 파일을 read.csv()로(즉 sep = ","로) 읽으면 오류 없이 조용히 잘못된 결과를 만들어 냅니다. 헤더 줄(제품;가격)에는 쉼표가 없어 열이 1개로 인식되는데, 데이터 줄(사과;1500,5)에는 쉼표가 있어 열이 2개로 나뉩니다. 이렇게 데이터의 열 개수가 헤더보다 하나 더 많으면 R은 첫 번째 열을 행 이름(row name)으로 간주해 버립니다.

wrong <- read.csv("eu.csv")
wrong
rownames(wrong)
#>             제품.가격
#> 사과;1500           5
#> 바나나;2300         0
#> 포도;4200          75
#> [1] "사과;1500"   "바나나;2300" "포도;4200"  

원래 필요했던 "제품"·"가격" 두 열이 아니라, 제품.가격이라는 열 하나(소수점 이하 자릿수만 남음)와 엉뚱한 행 이름만 남은 것을 볼 수 있습니다. read.csv2()로 읽으면 의도한 대로 처리됩니다.

correct <- read.csv2("eu.csv")
correct
#>     제품    가격
#> 1   사과 1500.50
#> 2 바나나 2300.00
#> 3   포도 4200.75

read.fwf()

read.fwf(file, widths, header = FALSE, skip = 0, col.names, colClasses = NA, strip.white = FALSE, ...)는 구분자 없이 자리(고정폭, fixed width)로만 값이 나뉘어 있는 파일을 읽습니다("fixed width file"의 약자).

  • file : 읽어 들일 파일 경로.
  • widths : 각 열이 차지하는 문자 수(폭)를 나타내는 정수 벡터. 예를 들어 c(4, 3, 4)는 "처음 4글자는 1열, 다음 3글자는 2열, 다음 4글자는 3열"이라는 뜻입니다. 음수를 넣으면 그만큼의 글자를 건너뛰고(무시하고) 넘어갑니다.
  • header, skip : read.table()과 동일합니다.
  • col.names : 각 열에 붙일 이름. 지정하지 않으면 V1, V2, ... 형태의 기본 이름이 붙습니다.
  • colClasses : read.table()과 동일하게 열의 자료형을 미리 지정합니다.
  • strip.white : TRUE로 지정하면 각 값의 앞뒤 공백을 잘라내려 시도합니다. 다만 실제로는 큰따옴표로 감싸지 않은 값에만 부분적으로 적용되므로, 아래 예제처럼 trimws()로 직접 다듬는 편이 더 확실합니다.

사원 정보가 구분자 없이 정해진 자리마다 값이 채워진 파일(사원번호 4자리, 이름 3자리, 부서 4자리)이 있다고 하겠습니다.

1001김민준영업  
1002이서연기획  
1003박도윤총무  
d <- read.fwf("사원.txt", widths = c(4, 3, 4),
              col.names = c("사원번호", "이름", "부서"))
d
#>   사원번호   이름   부서
#> 1     1001 김민준 영업  
#> 2     1002 이서연 기획  
#> 3     1003 박도윤 총무  

부서 열의 값 끝에 원본 파일의 여백이 그대로 남아 있는 것을 볼 수 있습니다. trimws()(12장)로 직접 다듬어 줍니다.

d$부서 <- trimws(d$부서)
d
#>   사원번호   이름 부서
#> 1     1001 김민준 영업
#> 2     1002 이서연 기획
#> 3     1003 박도윤 총무

한글처럼 한 글자가 화면에서 두 칸을 차지하는 문자가 섞인 파일의 폭을 셀 때는, widths가 화면상의 "칸 수"가 아니라 문자(글자) 수 기준이라는 점에 유의해야 합니다.

scan()

scan(file = "", what = double(), sep = "", na.strings = "NA", skip = 0, nlines = 0, quiet = FALSE, text, ...)는 파일이나 콘솔에서 데이터를 읽어 벡터(또는 지정한 구조)로 반환하는, 이 절에서 가장 저수준(low-level)인 함수입니다. 실제로 read.table() 내부에서도 scan()이 쓰입니다.

  • file : 읽어 들일 파일 경로. 빈 문자열(기본값)이면 콘솔에서 직접 입력을 받습니다(대화형 세션에서만 의미가 있습니다).
  • what : 읽어 들일 값의 자료형을 알려 주는 "본보기"입니다. 기본값 double()은 숫자로 읽으라는 뜻이고, character()를 지정하면 문자형으로 읽습니다. list(...)를 넘기면 여러 열을 한 번에 각기 다른 자료형으로 읽을 수도 있습니다.
  • sep : 값을 구분하는 구분자. 기본값 ""은 공백류 문자를 구분자로 봅니다.
  • na.strings : read.table()과 동일합니다.
  • skip, nlines : 건너뛸 줄 수와 읽어 들일 최대 줄 수.
  • quiet : FALSE(기본값)이면 몇 개의 값을 읽었는지("Read N items")를 메시지로 알려 줍니다. 스크립트에서 이 메시지가 거슬린다면 TRUE로 지정합니다.
  • text : 파일 대신 문자열 자체에서 곧바로 읽고 싶을 때 사용합니다(file 대신 지정).
# 숫자.txt 내용: "10 20 30" / "40 50 60" (두 줄)
x <- scan("숫자.txt")
x
#> [1] 10 20 30 40 50 60

콘솔에는 몇 개의 값을 읽었는지 알려 주는 메시지(Read 6 items)가 함께 표시됩니다. 파일이 아니라 문자열 자체를 바로 읽어야 할 때는 text 인자가 편리합니다.

y <- scan(text = "사과 바나나 포도", what = "character")
y
#> [1] "사과"   "바나나" "포도"  

read.table()이 이미 데이터프레임이라는 완성된 형태로 결과를 주는 반면, scan()은 더 단순하고 유연한 대신 구조를 직접 만들어야 하는 저수준 함수입니다. 정형화된 표 형태의 자료라면 read.table() 계열이, 벡터 하나만 빠르게 읽으면 되는 상황이라면 scan()이 더 간단합니다.

readLines()

readLines(con, n = -1L, encoding = "unknown", warn = TRUE)는 텍스트 파일을 값 단위가 아니라 줄(line) 단위 그대로, 문자형 벡터로 읽어 들입니다.

  • con : 읽어 들일 파일 경로 또는 연결(connection) 객체.
  • n : 읽어 들일 최대 줄 수. 기본값 -1L은 파일 끝까지 전부 읽으라는 뜻입니다.
  • encoding : 파일의 문자 인코딩을 지정합니다.
  • warn : TRUE(기본값)이면 파일 끝에 개행 문자가 없는 등 형식이 살짝 어긋난 경우 경고를 띄웁니다.

read.table()·scan()이 값을 특정 자료형(숫자·문자 등)으로 해석해서 읽는 반면, readLines()는 해석을 전혀 하지 않고 각 줄을 있는 그대로 문자열로 가져온다는 점이 다릅니다. 그래서 정형화되지 않은 텍스트(로그 파일, 설정 파일, 형식이 일정하지 않은 문서 등)를 다룰 때, 또는 본격적으로 파싱하기 전에 파일의 실제 내용을 먼저 눈으로 확인하고 싶을 때 유용합니다.

lines <- readLines("성적.csv")
lines

readLines("성적.csv", n = 2)   # 처음 두 줄만
#> [1] "\"이름\",\"국어\",\"영어\",\"수학\"" "\"김민준\",88,92,79"                
#> [3] "\"이서연\",95,88,84"                 "\"박도윤\",76,90,95"                
#> [5] "\"최지우\",90,74,88"                 "\"정하은\",82,85,91"                
#> [1] "\"이름\",\"국어\",\"영어\",\"수학\"" "\"김민준\",88,92,79"                

더 알아보기: 한글 파일과 인코딩 문제

한국에서 만들어진 텍스트·CSV 파일은 UTF-8뿐 아니라 CP949(구 EUC-KR 계열, 흔히 "Windows-949"라고도 함)로 저장된 경우가 여전히 많습니다. 특히 Windows 엑셀에서 "CSV(쉼표로 분리)" 형식으로 저장하면 기본적으로 CP949로 저장되고, "CSV UTF-8(쉼표로 분리)" 형식으로 저장해야 UTF-8이 됩니다. 두 인코딩이 섞이면 한글이 깨지거나, 아래처럼 아예 오류가 나기도 합니다.

tryCatch(
  read.csv("cp949.csv"),   # UTF-8 세션에서 CP949로 저장된 파일을 그냥 읽으면
  error = function(e) cat("에러:", conditionMessage(e), "\n")
)
#> 에러: invalid multibyte string at '<c0>><a7>' 

fileEncoding 인자에 파일의 실제 인코딩을 지정하면 해결됩니다.

read.csv("cp949.csv", fileEncoding = "CP949")
#>     이름 점수
#> 1 김민준   88
#> 2 이서연   95

파일이 어떤 인코딩인지 전혀 모르겠다면, readLines(..., encoding = "CP949")처럼 몇 가지 후보를 바꿔가며 원문이 제대로 보이는지 눈으로 확인해 보는 것도 실용적인 방법입니다. 한글 인코딩 문제는 R 자체의 결함이라기보다는 운영체제·프로그램마다 기본 인코딩이 다르기 때문에 생기는 문제이므로, 원본 파일이 어떤 환경에서 만들어졌는지 확인하는 것이 가장 확실한 해결책입니다.

더 알아보기: 인터넷 주소(URL)에서 곧바로 읽기

read.table()·read.csv()의 file 인자에는 로컬 파일 경로뿐 아니라 "https://"로 시작하는 웹 주소도 그대로 넣을 수 있습니다. 파일을 먼저 내려받지 않고도 곧바로 데이터프레임으로 읽어 들일 수 있어, 공개된 데이터셋을 다룰 때 특히 편리합니다.

url <- "https://raw.githubusercontent.com/vincentarelbundock/Rdatasets/master/csv/datasets/iris.csv"
iris_web <- read.csv(url)
head(iris_web, 3)
#>   rownames Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> 1        1          5.1         3.5          1.4         0.2  setosa
#> 2        2          4.9         3.0          1.4         0.2  setosa
#> 3        3          4.7         3.2          1.3         0.2  setosa

CSV처럼 즉시 해석할 파일이 아니라 이미지·zip처럼 통째로 내려받아 저장부터 해야 하는 파일이라면 download.file(url, destfile, mode = "wb")을 사용합니다. 텍스트가 아닌 바이너리 파일은 mode = "wb"(write binary)를 꼭 지정해야 파일이 깨지지 않습니다.

download.file(url, destfile = "iris_다운로드.csv", quiet = TRUE)
file.exists("iris_다운로드.csv")
#> [1] TRUE

사내망 등 인터넷이 연결되지 않은 환경에서는 이 두 함수 모두 사용할 수 없으니, 방화벽·프록시 설정을 먼저 확인해야 합니다.

readRDS()

readRDS(file, refhook = NULL)는 R 객체 하나를 그대로 저장해 둔 .rds 파일을 읽어 들여 원래의 R 객체로 복원합니다. 짝이 되는 saveRDS()와 함께 최근 R 실무에서 매우 널리 쓰이는 함수입니다.

  • file : 읽어 들일 .rds 파일 경로.
  • refhook : 참조(reference) 형태로 저장된 특수한 객체를 복원할 때 쓰는 고급 인자로, 일반적인 용도에서는 거의 쓸 일이 없습니다.

read.csv()·read.table()이 텍스트 형태의 표 자료만 다룰 수 있는 것과 달리, readRDS()는 데이터프레임은 물론 리스트·모형 객체(lm()의 결과 등)까지 R의 어떤 객체든 원래 모습 그대로(자료형·속성 포함) 복원할 수 있습니다. 예제 파일은 다음 코드로 만들어 둡니다(saveRDS()는 다음 절에서 자세히 다룹니다).

성적 <- read.csv("성적.csv")
성적$평균 <- round(rowMeans(성적[2:4]), 1)
saveRDS(성적, "성적.rds")

이렇게 저장된 파일은 다음처럼 복원합니다.

성적_복원 <- readRDS("성적.rds")
성적_복원
#>     이름 국어 영어 수학 평균
#> 1 김민준   88   92   79 86.3
#> 2 이서연   95   88   84 89.0
#> 3 박도윤   76   90   95 87.0
#> 4 최지우   90   74   88 84.0
#> 5 정하은   82   85   91 86.0

load()와 달리 readRDS()는 결과를 원하는 이름의 새 변수에 직접 할당할 수 있다는 점이 큰 장점입니다(load()는 저장 당시의 변수 이름을 그대로 복원하므로 이름을 선택할 수 없습니다).

14.2 파일 저장

14.1절에서 읽어 들인 성적 데이터에 평균 열을 추가했습니다. 이 결과를 화면에서 확인하는 것으로 끝내지 않고, 엑셀이나 다른 통계 프로그램에서 다시 열어 볼 수 있는 파일로 남기거나, 다음 R 세션에서 곧바로 이어서 쓸 수 있는 형태로 저장해야 하는 경우가 많습니다.

앞서 만든 평균 포함 데이터프레임을 CSV로 내보내 봅니다.

성적 <- read.csv("성적.csv")
성적$평균 <- round(rowMeans(성적[2:4]), 1)
write.csv(성적, "성적_결과.csv", row.names = FALSE)

14.1절의 읽기 함수들과 마찬가지로, 저장 함수도 "표 형태로 저장해 다른 프로그램과 주고받을 것인가(write.table() 계열)"와 "R 객체 자체를 그대로 저장해 R 세션에서 이어 쓸 것인가(save() 계열)"로 나뉩니다.

write.table()

write.table(x, file = "", sep = " ", row.names = TRUE, col.names = TRUE, quote = TRUE, na = "NA", dec = ".", fileEncoding = "")는 데이터프레임(또는 행렬) x를 텍스트 파일로 저장합니다. read.table()과 정확히 대응되는 함수입니다.

  • x : 저장할 데이터프레임 또는 행렬.
  • file : 저장할 파일 경로. 기본값 ""은 콘솔(화면)에 그대로 출력합니다.
  • sep : 값 사이를 구분할 기호. 기본값은 공백 한 칸입니다.
  • row.names : TRUE(기본값)이면 각 행 앞에 행 이름을 함께 저장합니다. 대부분의 경우 원치 않는 열이 하나 추가되는 셈이므로, 다른 프로그램과 주고받을 목적이라면 FALSE로 지정하는 것이 안전합니다.
  • col.names : TRUE(기본값)이면 첫 줄에 열 이름을 저장합니다.
  • quote : TRUE(기본값)이면 문자형 값을 큰따옴표로 감쌉니다.
  • na : 결측치(NA)를 파일에 어떤 문자열로 표시할지 지정합니다(기본값 "NA").
  • dec : 소수점 기호(기본값 ".").
  • fileEncoding : 저장할 파일의 문자 인코딩을 지정합니다. 지정하지 않으면 현재 R 세션의 기본 인코딩을 따릅니다.
write.table(성적, "성적_결과.txt", sep = "\t", row.names = FALSE, quote = FALSE)
cat(readLines("성적_결과.txt"), sep = "\n")
#> 이름   국어  영어  수학  평균
#> 김민준  88  92  79  86.3
#> 이서연  95  88  84  89
#> 박도윤  76  90  95  87
#> 최지우  90  74  88  84
#> 정하은  82  85  91  86

write.csv() / write.csv2()

write.table()과 마찬가지로, 자주 쓰는 옵션 조합을 미리 지정해 둔 wrapper 함수가 있습니다. write.csv()는 sep = ",", write.csv2()는 sep = ";"·dec = ","를 기본값으로 사용하며, 나머지는 write.table()과 동일합니다. 다만 두 함수 모두 row.names의 기본값이 TRUE이고, col.names는 별도로 지정해도 무시되며 경고("attempt to set 'col.names' ignored")가 뜬다는 점에 유의해야 합니다.

write.csv(성적, "성적_결과_기본.csv")   # row.names 기본값 TRUE
cat(readLines("성적_결과_기본.csv"), sep = "\n")
#> "","이름","국어","영어","수학","평균"
#> "1","김민준",88,92,79,86.3
#> "2","이서연",95,88,84,89
#> "3","박도윤",76,90,95,87
#> "4","최지우",90,74,88,84
#> "5","정하은",82,85,91,86

첫 열에 이름 없는 행 번호("1", "2", ...)가 그대로 따라온 것을 볼 수 있습니다. 데이터프레임의 행 이름이 의미 없는 단순 일련번호라면, 대부분 이 열은 필요하지 않으므로 row.names = FALSE를 지정하는 습관을 들이는 것이 좋습니다.

write.csv(성적, "성적_결과.csv", row.names = FALSE)
cat(readLines("성적_결과.csv"), sep = "\n")
#> "이름","국어","영어","수학","평균"
#> "김민준",88,92,79,86.3
#> "이서연",95,88,84,89
#> "박도윤",76,90,95,87
#> "최지우",90,74,88,84
#> "정하은",82,85,91,86

write.csv2()는 유럽식 표기를 그대로 다시 만들 때 사용합니다.

가격표 <- data.frame(제품 = c("사과", "바나나"), 가격 = c(1500.5, 2300.75))
write.csv2(가격표, "가격표_유럽식.csv", row.names = FALSE)
cat(readLines("가격표_유럽식.csv"), sep = "\n")
#> "제품";"가격"
#> "사과";1500,5
#> "바나나";2300,75

writeLines()

writeLines(text, con = stdout(), sep = "\n", useBytes = FALSE)는 문자형 벡터 text의 각 원소를 파일(또는 콘솔)에 한 줄씩 그대로 저장합니다. readLines()와 정확히 대응되는 함수입니다.

  • text : 저장할 문자형 벡터. 원소 하나가 한 줄이 됩니다.
  • con : 저장할 파일 경로 또는 연결 객체. 기본값 stdout()은 콘솔에 그대로 출력합니다.
  • sep : 각 줄 사이에 넣을 구분 기호(기본값은 줄바꿈 "\n").
  • useBytes : 인코딩 변환 없이 원래의 바이트를 그대로 쓸지 여부를 지정하는 고급 인자입니다.

write.table()·write.csv()가 표(데이터프레임) 형태를 저장하는 데 특화된 반면, writeLines()는 형식에 상관없이 문자열을 줄 단위로 그대로 저장한다는 점이 다릅니다. 표로 정리되지 않는 메모, 로그, 보고서 초안 등을 저장할 때 적합합니다.

메모 <- c("2026-07-21 회의록", "참석자: 김민준, 이서연", "다음 회의: 8월 첫째 주")
writeLines(메모, "회의록.txt")
cat(readLines("회의록.txt"), sep = "\n")
#> 2026-07-21 회의록
#> 참석자: 김민준, 이서연
#> 다음 회의: 8월 첫째 주

save() / save.image() / load()

save(..., file, list = character())는 지정한 R 객체 여러 개를 원래의 변수 이름 그대로 하나의 .RData 파일에 저장합니다. save.image(file = ".RData")는 현재 세션에 있는 모든 객체를 통째로 저장하는, save(list = ls(all.names = TRUE), file = ".RData")의 편의 버전입니다. load(file)은 이렇게 저장된 파일을 읽어 저장 당시의 변수 이름 그대로 현재 세션에 복원합니다.

  • ... : 저장할 객체들의 이름(따옴표 없이 나열).
  • file : 저장할(또는 불러올) .RData 파일 경로.
  • list : 저장할 객체 이름들을 문자형 벡터로 한꺼번에 지정하고 싶을 때 사용합니다(... 대신 사용).
a <- 1
b <- "hello"
save(a, b, file = "ab.RData")
rm(a, b)
exists("a")
#> [1] FALSE

load("ab.RData")
a
b
#> [1] 1
#> [1] "hello"

rm()으로 지워졌던 a, b가 load() 이후 원래 이름 그대로 되살아난 것을 볼 수 있습니다. RStudio를 종료할 때 뜨는 "작업공간을 저장하시겠습니까?" 대화상자가 바로 이 save.image()를 호출하는 것이며, 프로젝트 폴더에 남는 .RData 파일이 그 결과물입니다.

💡 실무에서는요? 세션 전체를 통째로 저장하는 save.image()는 편리하지만, 시간이 지나면 어떤 변수가 왜 들어 있는지 알기 어려워지고 파일 용량도 불필요하게 커지기 쉽습니다. 재현 가능한(reproducible) 분석을 위해서는 .RData에 의존하기보다, 필요한 객체만 아래의 saveRDS()로 개별 저장하거나 코드를 처음부터 다시 실행해 결과를 재현하는 방식이 권장됩니다. 최근 RStudio도 "종료 시 .RData에 작업공간 저장" 옵션을 기본적으로 꺼 두도록(off) 안내하고 있습니다.

saveRDS()

saveRDS(object, file, ascii = FALSE, compress = TRUE, refhook = NULL)는 R 객체 하나를 .rds 파일로 저장합니다. readRDS()와 정확히 대응됩니다.

  • object : 저장할 R 객체 하나.
  • file : 저장할 .rds 파일 경로.
  • ascii : TRUE로 지정하면 사람이 읽을 수 있는 텍스트(아스키) 형태로 저장합니다(기본값은 이진(binary) 형태인 FALSE).
  • compress : 파일을 압축할지 여부(기본값 TRUE).
  • refhook : readRDS()와 마찬가지로 고급 용도의 인자입니다.

save()가 변수 이름까지 함께(그 이름 그대로 복원되도록) 저장하는 반면, saveRDS()는 객체의 값만 저장하고 이름은 저장하지 않습니다. 그 덕분에 불러올 때 원하는 새 이름으로 자유롭게 할당할 수 있어, 이름 충돌 걱정 없이 여러 결과물을 관리하기에 더 안전합니다. 사용법은 readRDS() 항목에서 이미 살펴보았으므로, 여기서는 save()/load()와의 핵심 차이만 다시 확인해 보겠습니다.

rm(성적)
성적_새이름 <- readRDS("성적.rds")   # save()/load() 방식이었다면 이런 이름 변경이 불가능
성적_새이름
#>     이름 국어 영어 수학 평균
#> 1 김민준   88   92   79 86.3
#> 2 이서연   95   88   84 89.0
#> 3 박도윤   76   90   95 87.0
#> 4 최지우   90   74   88 84.0
#> 5 정하은   82   85   91 86.0

14.3 콘솔 출력

파일을 읽고 쓰는 것과는 별개로, 계산 과정이나 결과를 화면(콘솔)에 사람이 보기 좋은 형태로 보여줘야 할 때가 많습니다. R은 객체를 콘솔에 표시할 때 기본적으로 print()를 자동으로 호출하지만, 여러 값을 한 문장처럼 이어 붙이거나, 숫자의 자릿수·자리 구분 기호를 원하는 대로 맞추거나, 화면 대신 파일로 그 출력을 그대로 받아 내야 하는 경우에는 별도의 함수가 필요합니다.

"학생 이름과 평균 점수를 문장으로 이어 붙여 출력하기"와 "숫자를 천 단위 쉼표로 표시하기"라는 두 상황을 생각해 보겠습니다.

cat("이름:", "김민준", "/ 평균:", 86.3, "\n")
#> 이름: 김민준 / 평균: 86.3 

format(1234567, big.mark = ",")
#> [1] "1,234,567"

cat()은 여러 값을 사람이 읽기 편한 하나의 문장처럼 이어 붙여 출력하는 데, print()는 R 객체를 그 자료형에 맞는 표준 형식으로 출력하는 데, format()은 값 자체는 바꾸지 않고 출력용 문자열의 모양(자릿수·정렬·자리 구분 기호 등)만 다듬는 데 각각 특화되어 있습니다. sink()는 이 콘솔 출력의 "목적지"를 화면이 아니라 파일로 바꿔치기하는 함수입니다.

cat()

cat(..., file = "", sep = " ", fill = FALSE, append = FALSE)는 여러 값을 이어 붙여 콘솔(또는 파일)에 그대로 출력합니다.

  • ... : 출력할 값들. 벡터·문자열·숫자를 자유롭게 섞어 나열할 수 있습니다.
  • file : 출력할 파일 경로. 기본값 ""은 콘솔에 출력합니다.
  • sep : 값들 사이에 넣을 구분 기호(기본값은 공백 한 칸).
  • fill : TRUE(또는 숫자)로 지정하면 출력 폭에 맞춰 자동으로 줄바꿈합니다.
  • append : TRUE로 지정하면 기존 파일 내용 뒤에 이어서 씁니다(기본값 FALSE는 파일을 덮어씁니다).

print()와 달리 cat()은 따옴표나 [1] 같은 색인 표시 없이 값을 그대로 이어 붙인다는 점이 핵심입니다.

x <- "hello"
print(x)
#> [1] "hello"

cat(x, "\n")
#> hello 

sep 인자로 값들 사이의 구분 기호를 자유롭게 바꿀 수 있습니다.

cat("이름:", "김민준", "국어:", 88, "영어:", 92, sep = " / ")
cat("\n")
cat(1:5, sep = ", ")
cat("\n")
#> 이름: / 김민준 / 국어: / 88 / 영어: / 92
#> 1, 2, 3, 4, 5

주의: sep은 cat()에 나열한 인자들 "사이"에만 들어가는 것이 아니라, cat()에 나열된 모든 값을 하나의 긴 나열로 취급해 그 사이사이에 똑같이 끼워 넣습니다. 위 예제에서 "이름:"과 "김민준" 사이뿐 아니라 "김민준"과 "국어:" 사이에도 " / "가 들어간 것이 바로 그 예입니다. 값들을 의도한 자리에서만 구분하고 싶다면 paste()로 먼저 하나의 문자열을 만든 뒤 cat()에 넘기는 것이 안전합니다.

print()

print(x, ...)는 R 객체 x를 그 자료형에 맞는 표준 형식으로 출력합니다. 콘솔에 객체 이름만 입력했을 때 자동으로 호출되는 함수가 바로 이 print()입니다.

  • x : 출력할 객체.
  • ... : 객체의 클래스에 따라 다른 세부 옵션이 전달됩니다. 예를 들어 문자형 벡터에는 quote(따옴표 표시 여부), 숫자에는 digits(유효 자릿수) 등을 지정할 수 있습니다.
x <- c("사과", "바나나")
print(x)
#> [1] "사과"   "바나나"

print(x, quote = FALSE)
#> [1] 사과   바나나

data.frame·lm 모형처럼 클래스가 있는 객체를 print()에 넘기면, R은 그 클래스 전용의 print.data.frame()·print.lm() 같은 메서드를 자동으로 찾아 호출합니다(19.2절 S3 클래스에서 이 메서드 탐색 규칙을 자세히 다룹니다). 콘솔에 객체 이름만 쳤을 때 객체 종류마다 다른 모양으로 출력되는 이유가 바로 이 메서드 디스패치(dispatch) 덕분입니다.

format()

format(x, digits = NULL, nsmall = 0L, width = NULL, big.mark = "", scientific = NA, justify = "left", ...)는 값 자체는 바꾸지 않고, 출력용 문자열로 변환하면서 자릿수·정렬·자리 구분 기호 등의 모양을 다듬습니다.

  • x : 서식을 적용할 값(숫자·문자·날짜 등).
  • digits : 표시할 유효 자릿수.
  • nsmall : 소수점 이하 최소 자릿수. 정수만 지정하는 digits와 달리, 끝자리가 0이라도 자릿수를 채워 표시하고 싶을 때 사용합니다.
  • width : 결과 문자열의 최소 너비. 값이 이보다 짧으면 빈칸을 채워 맞춥니다.
  • big.mark : 천 단위 등 자리 구분에 사용할 기호(예: ",").
  • scientific : TRUE이면 지수 표기(예: 1e+05), FALSE이면 일반 표기를 강제합니다. 기본값 NA는 R이 상황에 맞게 자동으로 선택합니다.
  • justify : 문자형 값의 정렬 방향("left"(기본값)·"right"·"centre").
format(3.14159, digits = 3)
#> [1] "3.14"

format(3.1, nsmall = 3)
#> [1] "3.100"

format(1234567, big.mark = ",")
#> [1] "1,234,567"

width를 지정하면 여러 값의 자릿수를 맞춰 표 형태로 나란히 보여 줄 때 특히 유용합니다.

format(c(1, 10, 100), width = 5)
#> [1] "    1" "   10" "  100"

날짜에도 그대로 적용되어, 원하는 표기 형식으로 바꿀 수 있습니다.

format(Sys.Date(), "%Y년 %m월 %d일")
#> [1] "2026년 07월 21일"

💡 format()과 formatC(), 뭐가 다를까요? formatC()도 값을 서식 문자열로 바꾸는 함수라는 점은 같지만, C 언어의 printf 서식 규칙(%d, %f, %e 등)에 더 가까운 세부 제어를 제공합니다. 예를 들어 정수 앞을 0으로 채우거나(flag = "0"), 지수 표기의 자릿수를 정확히 지정하고 싶을 때는 formatC()가 더 직관적입니다.

formatC(7, width = 3, flag = "0")             # 앞을 0으로 채움
formatC(0.000123, format = "e", digits = 2)   # 지수 표기
#> [1] "007"
#> [1] "1.23e-04"

반대로 벡터 전체의 자릿수를 데이터에 맞춰 자동으로 정렬해 주는 것은 format() 쪽이 더 편리합니다. 두 함수 모두 결과가 문자형이라는 점은 같으므로, 서식을 적용한 값을 다시 숫자로 계산에 쓰려면 as.numeric()으로 되돌려야 합니다.

sink()

sink(file = NULL, append = FALSE, type = c("output", "message"), split = FALSE)는 콘솔에 출력될 내용을 화면 대신 파일로 돌려보냅니다("output을 가라앉힌다"는 의미의 이름입니다).

  • file : 출력을 받아 낼 파일 경로. NULL(기본값)을 지정하면 다시 콘솔 출력으로 되돌립니다.
  • append : TRUE로 지정하면 기존 파일 내용 뒤에 이어서 씁니다.
  • type : "output"(기본값, 일반 출력)과 "message"(경고·메시지) 중 어느 것을 파일로 돌려보낼지 지정합니다.
  • split : TRUE로 지정하면 파일에 저장하면서 동시에 콘솔에도 출력합니다(기본값 FALSE는 파일에만 저장).

sink()가 실행되고 있는 동안에는 print()·cat()을 포함한 거의 모든 콘솔 출력이 지정한 파일로 저장되며, 인자 없이 sink()를 한 번 더 호출해야 다시 화면으로 돌아옵니다. 이 짝을 맞추지 않으면 이후의 모든 출력이 계속 파일로 사라져 버리므로, sink()를 열었다면 반드시 닫아야 한다는 점을 기억해야 합니다.

sink("결과.txt")
cat("이것은 파일에 저장됩니다.\n")
print(summary(1:10))
sink()   # 다시 콘솔로 복귀

cat("콘솔로 복귀했습니다. 파일 내용:\n")
cat(readLines("결과.txt"), sep = "\n")
#> 콘솔로 복귀했습니다. 파일 내용:
#> 이것은 파일에 저장됩니다.
#>    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#>    1.00    3.25    5.50    5.50    7.75   10.00 

주의: 코드 중간에 오류가 나서 sink()를 닫는 코드가 실행되지 못하면, 이후 콘솔에 아무것도 출력되지 않는 것처럼 보여 당황하기 쉽습니다. sink()는 인자 없이 여러 번 호출해도 안전하므로(더 이상 열려 있는 sink가 없으면 아무 일도 일어나지 않음), while (sink.number() > 0) sink()처럼 남아 있는 모든 sink를 확실히 닫아 주는 방어적인 코드를 함께 써 두면 안전합니다.