파일 경로 오류(FileNotFoundError) 해결 총정리 — 상대경로·공백·백슬래시

분명 파일은 그 자리에 있는데 스크립트는 없다고 합니다. 파일 경로 오류는 입문자가 가장 자주 만나는 오류이면서, 동시에 경력이 쌓여도 환경이 바뀔 때마다 다시 만나는 오류입니다. 어제까지 잘 돌던 스크립트가 작업 스케줄러에 등록하자마자 FileNotFoundError를 뱉는 상황이 대표적입니다. 한 번 막히면 보통 20~30분은 날아가는데, 원인은 대부분 네댓 가지 중 하나입니다. 이 글에서는 그 원인을 증상별로 갈라내는 법과, 애초에 이 오류가 생기지 않는 코드 작성 습관을 정리합니다.

오류 메시지에서 이미 절반은 알려줍니다

가장 흔한 형태는 파이썬의 FileNotFoundError입니다. 메시지를 대충 넘기지 말고 마지막 줄의 따옴표 안을 보세요.

Traceback (most recent call last):
  File "main.py", line 3, in <module>
    with open("data.csv") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'data.csv'

여기서 중요한 건 'data.csv'가 상대 경로라는 점입니다. 상대 경로는 “현재 작업 디렉터리(current working directory) 기준”이라는 뜻이고, 그 현재 폴더가 여러분이 생각하는 폴더가 아닐 가능성이 큽니다. 반대로 메시지에 C:\Users\...\data.csv처럼 절대 경로가 찍혀 있다면 이야기가 달라집니다. 그때는 경로 해석이 아니라 철자·확장자·권한 쪽을 의심해야 합니다.

메시지에 찍힌 경로가장 유력한 원인먼저 확인할 것
data.csv (상대 경로)작업 디렉터리가 예상과 다름os.getcwd() 출력
C:/…/data.csv (절대 경로)철자·확장자 오타, 파일이 실제로 없음탐색기에서 확장자 표시 켜고 대조
경로 중간이 잘려 있음백슬래시가 이스케이프 문자로 해석됨raw 문자열(r"…") 사용
경로가 공백에서 끊김따옴표 없이 명령줄에 전달인자를 리스트로 전달
PermissionError로 바뀜파일이 아니라 폴더를 열었거나 사용 중엑셀 등에서 파일 열려 있는지 확인

1순위 원인 — “현재 폴더”가 내 생각과 다르다

상대 경로는 스크립트 파일이 있는 위치가 아니라 스크립트를 실행한 위치를 기준으로 풉니다. 이 둘이 다를 때 오류가 납니다. VS Code에서 실행 버튼을 누를 때는 프로젝트 루트가 작업 디렉터리인데, 작업 스케줄러는 기본값이 C:\Windows\System32입니다. 같은 스크립트가 손으로 돌리면 되고 자동 실행하면 실패하는 이유가 보통 이것입니다.

추측하지 말고 세 줄을 찍어서 확인하세요.

import os

# 지금 파이썬이 어디를 "현재 폴더"로 보고 있는지
print("작업 디렉터리:", os.getcwd())

# 그 폴더에 실제로 무엇이 있는지
print("폴더 내용:", os.listdir("."))

# 찾으려던 파일의 절대 경로는 어디로 해석됐는지
print("찾는 경로:", os.path.abspath("data.csv"))

os.path.abspath()가 알려주는 “실제로 찾으러 간 경로”를 보면 원인이 즉시 드러납니다. 파워셸이라면 Get-Location과 Resolve-Path가 같은 역할을 합니다.

근본 해결 — 스크립트 파일 위치를 기준점으로 고정하기

작업 디렉터리에 의존하지 않으려면 기준점을 코드 안에 박아두면 됩니다. 파이썬에서는 __file__과 pathlib을 조합하는 것이 표준적인 방법입니다.

from pathlib import Path

# __file__ = 지금 실행 중인 이 .py 파일의 위치
# .resolve()로 절대 경로를 확정하고, .parent로 그 파일이 든 폴더를 얻습니다.
BASE_DIR = Path(__file__).resolve().parent

# 이제 어디서 실행하든 같은 파일을 가리킵니다.
data_path = BASE_DIR / "data" / "sales.csv"

print(data_path)

with open(data_path, encoding="utf-8") as f:
    print(f.readline())

이렇게 써두면 바탕화면에서 실행하든, 스케줄러가 실행하든, 다른 폴더에서 python 프로젝트/main.py로 실행하든 항상 같은 파일을 가리킵니다. 기존 코드에서 open("data.csv")를 찾아 이 방식으로 바꾸는 것만으로 이 유형의 오류는 거의 사라집니다.

주피터 노트북에는 __file__이 없습니다. 노트북에서는 Path.cwd()를 쓰거나, 아예 절대 경로를 상수로 지정하는 편이 확실합니다.

2순위 원인 — 윈도우 백슬래시가 먹히는 문제

윈도우 경로의 \는 대부분의 언어에서 이스케이프 문자입니다. \t는 탭, \n은 줄바꿈으로 바뀌어버립니다. 탐색기에서 복사한 경로를 그대로 따옴표 안에 붙여 넣으면 이 사고가 납니다.

# 1) 잘못된 예 — \t가 탭 문자로, \n이 줄바꿈으로 해석됩니다.
path = "C:\temp\new\report.txt"   # 실제로는 C:<탭>emp<줄바꿈>ew...

# 2) raw 문자열 — 백슬래시를 글자 그대로 취급
path = r"C:\temp\new\report.txt"

# 3) 백슬래시를 두 번
path = "C:\\temp\\new\\report.txt"

# 4) 슬래시로 통일 — 윈도우 파이썬도 정상 인식합니다.
path = "C:/temp/new/report.txt"

# 5) 권장 — 조각을 넘기고 구분자는 라이브러리에 맡깁니다.
from pathlib import Path
path = Path("C:/temp") / "new" / "report.txt"

실무에서는 4번이나 5번을 권합니다. 슬래시(/)는 윈도우 파이썬에서도 정상 동작하고, pathlib은 실행 환경에 맞는 구분자를 알아서 붙여주기 때문에 나중에 리눅스 서버로 옮길 때 코드를 고칠 일이 없습니다.

3순위 원인 — 경로에 들어간 공백과 한글

D:\내 문서\보고서 초안.docx처럼 공백이 들어간 경로는 파이썬 문자열 안에서는 아무 문제가 없습니다. 문제는 그 경로를 명령줄로 넘길 때 생깁니다. 셸은 공백을 인자 구분자로 보기 때문에 경로 하나가 두 개의 인자로 쪼개집니다.

import subprocess
from pathlib import Path

src = Path(r"D:\내 문서\보고서 초안.docx")

# 잘못된 예 — 문자열을 직접 이어 붙이면 공백에서 인자가 쪼개집니다.
# subprocess.run("copy " + str(src) + " backup", shell=True)

# 올바른 예 — 리스트로 넘기면 각 원소가 인자 하나로 전달됩니다.
subprocess.run(["cmd", "/c", "copy", str(src), "backup"], check=True)

subprocess에 리스트로 넘기면 각 원소가 인자 하나로 전달되므로 따옴표를 직접 붙일 필요가 없습니다. 파워셸에서 직접 다룰 때는 변수에 담고 따옴표로 감싸는 것이 안전합니다.

# 파워셸 — 경로에 공백이 있으면 반드시 따옴표로 감쌉니다.
$src = "D:\내 문서\보고서 초안.docx"

# 존재 여부를 먼저 확인
Test-Path $src

# 상대 경로가 무엇으로 풀리는지 확인
Resolve-Path ".\data\sales.csv"

# 현재 위치
Get-Location

한글 경로 자체는 요즘 환경에서 대체로 문제없이 동작합니다. 다만 일부 오래된 CLI 도구나 인코딩 설정이 어긋난 환경에서는 경로가 깨져 “없는 파일”로 취급될 수 있습니다. 경로가 아니라 파일 내용이 깨져 보이는 문제라면 원인이 완전히 다르니, 인코딩 쪽을 따로 살펴봐야 합니다.

놓치기 쉬운 잔 원인들

  • 확장자 중복 — 윈도우 탐색기가 알려진 확장자를 숨기는 설정이면 data.csv로 보이는 파일의 실제 이름이 data.csv.txt일 수 있습니다. 보기 탭에서 “파일 확장명” 표시를 켜고 대조하세요.
  • 이름 끝의 공백 — 복사·붙여넣기 과정에서 파일명 끝에 공백이 붙는 경우가 있습니다. 눈으로는 구분이 안 되므로 os.listdir() 출력을 그대로 비교하는 편이 빠릅니다.
  • 대소문자 — 윈도우는 구분하지 않지만 리눅스 서버는 구분합니다. 로컬에서 되던 코드가 배포 후 실패하는 전형적인 원인입니다.
  • 네트워크 드라이브 — Z: 같은 매핑 드라이브는 로그인 세션에 묶여 있어서, 서비스나 스케줄러 계정에서는 보이지 않습니다. UNC 경로(\\서버명\공유폴더)를 쓰세요.
  • 쓰기인데 폴더가 없음 — 읽기는 파일이 없어서, 쓰기는 상위 폴더가 없어서 실패합니다. 메시지는 비슷하게 생겼지만 대처가 다릅니다.

쓰기 작업이라면 폴더부터 만들고 시작하세요

결과물을 날짜별 폴더에 저장하는 스크립트에서 특히 자주 걸립니다. open(path, "w")는 파일은 만들어주지만 폴더는 만들어주지 않습니다.

from pathlib import Path

out_dir = Path(__file__).resolve().parent / "output" / "2026-09"

# parents=True  : 중간 폴더까지 한 번에 만듭니다.
# exist_ok=True : 이미 있어도 오류를 내지 않습니다.
out_dir.mkdir(parents=True, exist_ok=True)

(out_dir / "result.txt").write_text("완료", encoding="utf-8")

parents=True와 exist_ok=True 두 옵션을 함께 쓰는 것이 관용적인 형태입니다. 중간 폴더까지 한 번에 만들고, 이미 있어도 조용히 넘어갑니다.

오류가 나기 전에 먼저 알려주는 코드

경로 문제는 사후에 추적하는 것보다 실행 직후에 걸러내는 편이 훨씬 빠릅니다. 스크립트 시작부에 이 정도만 넣어두면 원인 파악 시간이 크게 줄어듭니다.

from pathlib import Path
import sys

BASE_DIR = Path(__file__).resolve().parent
target = BASE_DIR / "data" / "sales.csv"

if not target.exists():
    print("파일을 찾지 못했습니다:", target)
    parent = target.parent
    if parent.exists():
        print("상위 폴더에는 이런 파일들이 있습니다:")
        for item in sorted(parent.iterdir()):
            print("  -", item.name)
    else:
        print("상위 폴더 자체가 없습니다:", parent)
    sys.exit(1)

print("확인 완료:", target)

파일이 없을 때 상위 폴더의 실제 목록을 함께 출력하는 것이 핵심입니다. 오타인지, 확장자가 다른 것인지, 폴더 자체를 잘못 짚은 것인지가 한눈에 갈립니다.

점검 순서 정리

  1. 오류 메시지의 경로가 상대 경로인지 절대 경로인지 확인합니다.
  2. 상대 경로라면 os.getcwd()와 os.path.abspath()를 찍어 실제로 어디를 보고 있는지 확인합니다.
  3. 경로 문자열에 \t, \n 같은 조합이 있는지 보고, 있으면 raw 문자열이나 슬래시로 바꿉니다.
  4. 탐색기에서 확장자 표시를 켜고 파일명을 글자 단위로 대조합니다.
  5. 해결된 뒤에는 Path(__file__).resolve().parent 기준으로 바꿔 재발을 막습니다.

마무리

지금 열려 있는 스크립트에서 open("으로 시작하는 줄을 검색해 보세요. 따옴표 안이 상대 경로라면 그 줄이 잠재적인 사고 지점입니다. 한 파일만이라도 BASE_DIR 방식으로 바꿔두면, 나중에 그 스크립트를 작업 스케줄러에 등록할 때 겪었을 디버깅 30분을 미리 아끼는 셈입니다.

다음 글에서는 자동화 스크립트가 실패했을 때 무슨 일이 있었는지 나중에라도 확인할 수 있도록, 파이썬 logging으로 실행 기록을 파일에 남기는 방법을 다루겠습니다.

댓글 남기기