write.csv()는 데이터 프레임을 내보냅니다
데이터 프레임을 R 밖으로 꺼내 다른 프로그램이 열 수 있는 파일로 만드는 일은 호출 한 번입니다:
write.csv(df, "results.csv", row.names = FALSE)
외워 둘 한 줄입니다 - 곧 이유를 설명할 row.names = FALSE까지 포함해서요. 첫 인자는 데이터 프레임, 두 번째는 파일 이름이며, 이 함수는 아무것도 반환하지 않습니다: 파일을 만드는 부수 효과가 이 함수의 일 전부입니다.
디스크를 건드리지 않고도 write.csv()가 무엇을 만드는지 정확히 볼 수 있습니다. 파일 이름을 기대하는 자리에 어떤 연결(connection)이든 받기 때문인데, 그냥 출력해 주는 stdout()도 여기에 포함됩니다:
여기 CSV가 있습니다: 헤더 한 줄, 행마다 한 줄, 텍스트 값은 따옴표로 감싸고 숫자는 맨몸입니다. (따옴표는 기본값 quote = TRUE 때문이며, 주요 CSV 리더는 모두 이를 처리하니 그대로 두세요.) 실제 파일로 쓰는 것은 stdout() 자리에 "results.csv"를 넣은 같은 호출입니다.
row.names = FALSE를 기본으로 삼아야 하는 이유
모든 데이터 프레임은 행 이름을 가집니다 - 보통은 1, 2, 3... 같은 숫자죠 - 그리고 write.csv()의 기본값은 이를 헤더가 빈 추가 첫 열로 파일에 쓰는 것입니다. 기본 출력이 어떤 모습인지, 그리고 그 파일이 R로 다시 돌아올 때 무슨 일이 벌어지는지 보세요:
내보내기 결과에 "1", "2"를 담은 이름 없는 첫 열이 생겼고, 다시 읽을 때 read.csv()는 뭐라도 이름을 붙여야 했기에 X를 만들어 냈습니다. 모든 초보자를 어리둥절하게 하는 그 정체불명의 X 열이 이것입니다: 데이터가 아니라 R의 행 번호가 파일로 새어 나온 것이죠. 내보내기·읽기 주기를 몇 번 반복하면 X, X.1, X.2...가 쌓이게 됩니다.
행 이름은 거의 언제나 실질적인 정보를 담지 않으며, 담는다면 그 정보는 어차피 제대로 된 열에 있어야 합니다. 그러니 row.names = FALSE를 반사 신경으로 만드세요. 사실 이것이 기본값이었어야 하지만, 하위 호환성 때문에 그렇지 않으니 습관은 여러분의 몫입니다.
파일이 저장되는 곳
write.csv(df, "results.csv")는 R의 작업 디렉터리 에 씁니다 - 그곳이 예상한 위치가 아니라면 파일이 사라진 것처럼 보입니다. 실패한 게 아니라 getwd()가 출력하는 폴더에 그냥 놓여 있을 뿐입니다. 읽기와 똑같은 함정을 거울에 비춘 것이고, 같은 두 도구로 해결됩니다:
getwd() # the folder your file went to
write.csv(df, "C:/Users/ada/results/out.csv", row.names = FALSE) # or: remove all doubt
전체 경로(슬래시는 Windows를 포함한 모든 플랫폼에서 동작합니다)를 쓰면 스크립트가 모호하지 않습니다. 작업 디렉터리를 설정하고 관리하는 방법은 작업 디렉터리에서 다룹니다.
다른 구분자를 위한 write.table() - 그리고 이어 쓰기 문제
write.csv()는 사실 범용 함수 write.table()을 쉼표에 맞춰 미리 설정한 얇은 래퍼입니다. 다른 형태가 필요하면 write.table()을 직접 호출하세요 - 가장 흔한 경우는 탭 구분입니다:
탭으로 구분되고 따옴표가 없습니다 - 많은 하류 도구가 선호하는 형식이죠. write.table()에서는 모든 것을 명시적으로 설정해야 한다는 점에 유의하세요. write.csv의 쉼표나 따옴표 기본값을 물려받지 않습니다.
write.csv()가 거부하는 한 가지는 이어 쓰기(append) 입니다. append = TRUE를 전달하면 그 설정이 무시되었다는 경고가 나오는데, 이어 쓰기를 하면 헤더 줄이 파일 중간에 다시 쓰이기 때문입니다. 기존 CSV에 정말로 행을 추가해야 한다면 - 예를 들어 실행마다 결과를 기록할 때 - write.table()로 내려가 헤더를 직접 억제하세요:
write.table(new_rows, "log.csv", sep = ",",
append = TRUE, col.names = FALSE, row.names = FALSE)
그리고 tidyverse를 쓴다면 readr::write_csv(df, "out.csv")가 read_csv()의 짝입니다: 같은 출력에 큰 데이터 프레임에서 더 빠르고, 애초에 행 이름을 절대 쓰지 않습니다 - 기억할 인자가 하나 줄어드는 셈이죠.
saveRDS()와 readRDS(): 정확한 왕복
CSV는 손실이 있는 형식입니다. 오직 텍스트만 저장하죠 - 그래서 Date 열은 "2026-08-07"이라는 문자열이 되고, 팩터는 수준 순서를 잃은 라벨이 되며, 속성은 사라지고, 임포트할 때 모든 타입을 다시 추측해야 합니다(대개는 맞지만 가끔은 틀립니다 - 앞자리 0, 바로 당신 이야기입니다). 게다가 CSV는 직사각형 표 하나만 담을 수 있습니다: 적합된 모형, 리스트, dimnames가 있는 행렬은 CSV로 표현할 방법 자체가 없습니다.
파일이 R을 위한 것 이라면 - 오늘 정제한 데이터를 내일 세션을 위해 저장하는 경우 - 텍스트 형식은 아예 건너뛰세요:
saveRDS(df, "clean_data.rds") # today
df <- readRDS("clean_data.rds") # tomorrow: identical object, types intact
saveRDS()는 R 객체 하나를, 어떤 객체든, 정확하게 직렬화합니다. readRDS()는 그것을 바이트 단위로 되돌려 줍니다: 팩터는 수준을 유지하고, 날짜는 여전히 날짜이며, 아무것도 다시 추측되지 않습니다. 데이터 프레임, 데이터 프레임의 리스트, 적합된 회귀 모형 등 무엇에든 동작합니다. .rds 파일은 압축된 바이너리라 대개 CSV보다 작기도 합니다. 유일한 비용은 R 외에는 아무것도 열 수 없다는 점인데, 바로 그래서 경험칙은 이렇습니다: 사람과 다른 도구에 공유할 때는 CSV, 자신을 위해 저장할 때는 RDS.
save()와 load() - 그리고 대개 RDS가 이기는 이유
더 오래된 짝도 만나게 됩니다. save()는 이름 있는 여러 객체 를 하나의 .RData 파일에 쓰고, load()는 그것들을 원래 이름으로 여러분의 작업 공간에 복원합니다:
save(df, model, params, file = "session.RData")
load("session.RData") # df, model, params silently appear
그 편리함이 곧 문제입니다. 변수 이름을 여러분이 아니라 load()가 정합니다 - 보이지 않는 채로 객체를 환경에 심고, 같은 이름이 이미 있었다면 조용히 덮어씁니다. 여섯 달 뒤에는 session.RData에 무엇이 들어 있는지 불러와 보기 전에는 아무도 기억하지 못합니다.
readRDS()의 인터페이스는 솔직합니다: 이름을 여러분이 고르고(df <- readRDS(...)), 등 뒤에서 덮어쓰는 일이 없으며, 파일 하나가 객체 하나를 뜻합니다. 여러 객체를 한 파일에 묶어야 하는 특별한 이유가 없다면 RDS를 쓰세요. (묶음이 정말 필요하다면 객체들을 이름 있는 리스트에 담아 saveRDS()하면 놀랄 일 없이 같은 효과를 얻습니다.)
핵심 정리
write.csv(df, "out.csv", row.names = FALSE)- 내보내기의 전부이며, 네, 언제나row.names = FALSE입니다.- 다시 읽을 때 나타나는 정체불명의
X열은 행 이름이 파일로 새어 나온 것입니다. 기본값이 그것을 썼을 뿐, 여러분은 그러지 마세요. - 파일은 작업 디렉터리에 저장됩니다 -
getwd()가 "잃어버린" 내보내기 파일을 찾아 주고, 전체 경로가 애초에 그런 일을 막아 줍니다. write.table()은 다른 구분자와 (col.names = FALSE와 함께) 진짜 이어 쓰기를 처리합니다.write.csv()는 의도적으로 이어 쓰기를 못 합니다.saveRDS()/readRDS()는 어떤 R 객체든 정확히 왕복시킵니다 - 타입, 팩터, 속성이 그대로입니다. 공유에는 CSV, 자신에게는 RDS.save()/load()보다 RDS를 선호하세요: 이름이 명시적이고 조용한 덮어쓰기가 없습니다.
다음 주제: 임포트에서 여러분을 물기 전에 별도의 페이지를 가질 자격이 있는 데이터 타입 - 날짜입니다.
자주 묻는 질문
R에서 데이터 프레임을 CSV로 어떻게 내보내나요?
write.csv(df, "output.csv", row.names = FALSE)입니다. row.names = FALSE 부분이 중요합니다: 이것이 없으면 R이 내부 행 번호를 첫 열로 추가해서 쓰고, 나중에 누군가 파일을 다시 읽을 때 X라는 정체불명의 열로 나타납니다.
내보낸 CSV를 다시 읽으면 왜 X 열이 생기나요?
write.csv의 기본값이 row.names = TRUE이기 때문입니다 - 행 번호를 이름 없는 첫 열로 썼고, 다시 읽을 때 read.csv가 그 열에 X라는 이름을 붙인 것입니다. row.names = FALSE로 내보내면 유령 열이 사라집니다.
R에서 write.csv와 saveRDS의 차이는 무엇인가요?
write.csv는 누구나 열 수 있는 평문 표를 만들지만 텍스트만 저장합니다 - 팩터 수준, Date 타입, 속성, 표가 아닌 객체는 평탄화되거나 사라지고, 임포트할 때 타입을 다시 추측해야 합니다. saveRDS는 R 객체 하나를 바이트 단위로 저장하고 readRDS가 그대로 되돌려 줍니다. 남과 공유할 때는 CSV, 자신의 R 세션을 위해 저장할 때는 RDS를 쓰세요.
write.csv는 파일을 어디에 저장하나요?
전체 경로를 주지 않으면 R의 현재 작업 디렉터리에 저장합니다. getwd()를 실행해 그곳이 어디인지 확인하세요 - 파일이 사라진 것 같다면 getwd()가 출력하는 폴더에 들어 있습니다. "C:/Users/ada/results/out.csv" 같은 전체 경로를 주면 모호함이 사라집니다.