PHP에서 가장 자주 만나는 오류 중 하나가 Warning: Cannot modify header information - headers already sent다. 결론부터 말하면, HTTP 헤더(쿠키·세션·리다이렉트)를 보내기 전에 이미 본문(HTML, 공백, 출력)이 먼저 전송되었기 때문이다. 원인을 알면 대부분 1분 안에 해결된다.

왜 발생하나

HTTP 응답은 헤더 → 본문 순서로 전송된다. PHP가 echo, HTML 한 줄, 심지어 공백 한 칸이라도 출력하는 순간 헤더 전송이 끝나버린다. 그 뒤에 header(), setcookie(), session_start() 같은 헤더 조작 함수를 호출하면 이 경고가 뜬다.

오류 메시지 끝에는 보통 원인 위치가 친절히 적혀 있다.

Warning: Cannot modify header information - headers already sent by
(output started at /var/www/html/config.php:1) in /var/www/html/login.php on line 12

output started at config.php:1 이 부분이 실제로 출력을 시작한 파일과 줄 번호다. 여기를 먼저 본다.

가장 흔한 3가지 원인

원인증상해결
여는 태그 앞 공백/빈 줄<?php 앞에 공백 존재파일 맨 앞 공백 제거
닫는 태그 ?> 뒤 줄바꿈include 파일 끝에 ?>\n닫는 태그 자체를 삭제
BOM(UTF-8 with BOM)눈에 안 보이는 3바이트BOM 없는 UTF-8로 재저장

특히 ?> 뒤 줄바꿈BOM은 눈에 보이지 않아 가장 까다롭다.

해결 방법

1. include 파일의 닫는 태그를 없앤다

PHP만 있는 파일이라면 닫는 태그 ?>는 아예 쓰지 않는 것이 공식 권장 방식이다. 뒤에 실수로 붙은 공백·줄바꿈을 원천 차단한다.

<?php
// config.php — 닫는 태그(?>)를 쓰지 않는다
define('DB_HOST', 'localhost');
define('DB_NAME', 'mydb');
// 파일 끝. ?> 없음

2. BOM 제거

리눅스에서 BOM이 있는지 바로 확인할 수 있다.

# 파일 첫 3바이트가 ef bb bf 면 BOM
head -c 3 config.php | xxd

# sed로 BOM 제거
sed -i '1s/^\xEF\xBB\xBF//' config.php

3. 헤더는 출력보다 먼저

session_start()header()는 어떤 출력보다도 앞에 와야 한다.

<?php
session_start();                 // 출력 전에 호출
header('Content-Type: text/html; charset=utf-8');

if (!isset($_SESSION['user'])) {
    header('Location: /login.php');
    exit;                        // 리다이렉트 뒤엔 반드시 exit
}

4. 급할 때는 출력 버퍼링

구조를 당장 못 고치는 상황이라면 출력 버퍼링으로 임시 회피할 수 있다. 다만 근본 해결은 위 1~3번이다.

<?php
ob_start();   // 출력을 버퍼에 모았다가 마지막에 전송
// ... 코드 ...
ob_end_flush();

점검 체크리스트

  • [ ] 오류 메시지의 output started at 위치를 먼저 확인했는가
  • [ ] include 파일들의 닫는 태그 ?>를 제거했는가
  • [ ] 파일을 BOM 없는 UTF-8로 저장했는가
  • [ ] header()·session_start()를 모든 출력보다 앞에 두었는가
  • [ ] 리다이렉트 header('Location: ...') 뒤에 exit;를 넣었는가

대부분은 닫는 태그 뒤 줄바꿈이나 BOM이 범인이다. 이 두 가지만 정리해도 같은 오류를 다시 만날 일이 크게 줄어든다.