CodeIgniter 4 프레임워크로 블로그를 처음부터 만들어 보는 학습용 프로젝트입니다. 정적 페이지와 라우팅부터 시작해 글 CRUD, 인증(Shield), 댓글, 카테고리·검색, 마크다운·이미지, 그리고 production 배포까지 한 흐름으로 다룹니다.
강좌는 회차(ep) = 하나의 커밋 + 하나의 태그 단위로 진행됩니다. 전체 회차별 작업 목록은 회차 태그(
ep01~ep30)와 커밋 이력에서 볼 수 있습니다.
Note
강좌 완성본 = ep01~ep30 태그 / 실제 운영 코드 = main 최신 커밋
강좌 30회차는 각 태그로 동결되어 있습니다(예: git checkout ep07). main은 강좌 완성(ep30) 이후 실제 블로그 운영을 위해 계속 업데이트되므로, 회차별 학습 내용을 보려면 태그를 체크아웃하세요.
- 라우트 → 컨트롤러 → 뷰로 이어지는 CI4의 기본 요청 흐름 이해
- 마이그레이션·시더·Model·Entity로 데이터 계층 구성
- Feature 테스트를 활용한 "실패 → 구현 → 통과" 개발 사이클
- 공식 인증 라이브러리 Shield로 로그인·권한·가드 구현
- 검증·CSRF·플래시 메시지를 갖춘 안전한 CRUD
- 마크다운 렌더링, 이미지 업로드, 그리고 production 배포까지
- PHP 8.2 이상
- CodeIgniter 4 (appstarter 기본 구조)
- Composer (의존성 관리)
- Shield — CodeIgniter 공식 인증 라이브러리
- PHPUnit — Feature/Unit 테스트
- 데이터베이스: 개발은 MySQL/MariaDB 또는 SQLite, 운영은 SQLite (테스트는 SQLite 메모리)
- PHP 8.2 이상 (
intl,mbstring등 CI4 권장 확장 포함) - Composer
- 데이터베이스 (개발: MySQL/MariaDB 또는 SQLite / 운영: SQLite)
# 1) 저장소 클론
git clone <repository-url> ci4blog
cd ci4blog
# 2) 의존성 설치
composer install
# 3) 환경 설정
cp env .env
# .env 편집: CI_ENVIRONMENT = development, app.baseURL, database.default.* 설정
# 4) 마이그레이션 & 시더
php spark migrate
php spark db:seed DatabaseSeeder
# 5) 개발 서버 실행
php spark serve브라우저에서 http://localhost:8080 으로 접속합니다.
composer test
# 또는
./vendor/bin/phpunit운영 환경은 개발과 두 가지가 다릅니다 — 디버그를 끄고(CI_ENVIRONMENT = production), 설정은 .env 로 주입합니다. 코드(app/Config/*)의 기본값은 그대로 두고, 서버의 .env 가 그 위를 덮습니다.
# 1) 코드 받기 & 운영용 의존성만 설치(dev 패키지 제외)
git pull --ff-only origin main
composer install --no-dev --optimize-autoloader
# 2) 운영 .env 작성
cp env.production.example .env
# app.baseURL = 'https://내도메인/'
# database.default.* = 운영 DB 접속 정보(비밀번호는 .env 에만)
php spark key:generate # encryption.key 생성
# 3) 마이그레이션
php spark migrate --all
# 4) 캐시 정리(설정/라우트 변경 후)
php spark cache:clear-
.env의CI_ENVIRONMENT = production— 디버그 툴바·상세 에러 페이지가 꺼졌는지 확인 -
app.baseURL이 실제 도메인(끝에/)이고 HTTPS 인지 -
app.forceGlobalSecureRequests = true(HTTPS 강제),app.indexPage = ''(깨끗한 URL) -
encryption.key가 채워졌는지(php spark key:generate) - 웹 서버의 document root 가
public/인지 (그 위 디렉터리가 공개되면 안 됨) -
.env와 운영 비밀값이 저장소·CI 에 올라가지 않는지
보안 설정은 두 곳에서 온다. 응답 헤더는 코드가 항상 붙이므로
.env와 무관하고,.env가 좌우하는 것은 HTTPS 리다이렉트와 쿠키Secure뿐입니다..env를 빠뜨려도 헤더가 통째로 사라지지는 않습니다.
| 설정 | 어디서 결정되나 |
|---|---|
X-Content-Type-Options·Referrer-Policy·X-Frame-Options |
코드 (app/Filters/SecurityHeaders.php, 전역 after 필터) — 항상 |
Strict-Transport-Security |
코드 — HTTPS 요청일 때만 |
Content-Security-Policy-Report-Only |
코드 (app/Config/Csp.php) |
HTTPS 강제 (app.forceGlobalSecureRequests) |
.env |
쿠키 Secure (cookie.secure) |
.env |
writable/ 아래(cache, logs, session, uploads, backups)는 웹 서버가 쓸 수 있어야 합니다. 소유자를 웹 서버 사용자로 두거나 그룹 쓰기를 허용합니다.
# 예: 웹 서버가 www-data 인 경우
sudo chown -R www-data:www-data writable/
sudo find writable/ -type d -exec chmod 775 {} \;
sudo find writable/ -type f -exec chmod 664 {} \;업로드 이미지는 웹 루트 밖(
writable/uploads/)에 저장되고Posts::image컨트롤러로 서빙되므로,writable/가 외부에서 직접 접근되지 않습니다.
배포(scripts/deploy.sh)는 마이그레이션 직전에 DB 스냅샷을 만듭니다. 백업이 실패하면 배포가 그 자리에서 멈춥니다 — 되돌릴 파일 없이 마이그레이션을 실행하지 않기 위해서입니다.
php spark db:backup # 스냅샷 1개 생성, 최근 10개 유지
php spark db:backup --keep 30 # 보관 개수 조정- 위치:
writable/backups/backup-<타임스탬프>.sqlite(타임스탬프는 UTC — 이름 정렬이 곧 시간 정렬이어야 오래된 것부터 지울 수 있습니다) - 방식:
VACUUM INTO— 쓰기 중에도 일관된 단일 파일 스냅샷을 만듭니다(-wal·-shm동반 파일이 없어 복구가 단순합니다). - SQLite 가 아닌 DB 에서는 이유를 출력하고 건너뜁니다(파일 복사로 백업할 대상이 아니므로).
백업 파일이 존재하는 환경은 곧 SQLite 를 쓰는 환경입니다(다른 드라이버에서는 커맨드가 건너뜁니다). 즉 .env 의 database.default.DBDriver = SQLite3 이고, database.default.database 가 DB 파일 경로입니다 — 아래 절차의 대상이 그 파일입니다.
- 웹 서버/PHP-FPM 을 멈춥니다.
- 현재 DB 파일(
.env의database.default.database경로)을 다른 이름으로 옮겨 둡니다 — 원인 조사에 필요합니다. - 되돌릴 백업 파일을 그 경로로 복사합니다.
- 파일 소유자를 PHP-FPM 실행 사용자로 맞춥니다(
chown). - 서비스를 다시 올리고
GET /health가{"status":"ok","db":"ok"}인지 확인합니다.
배포(scripts/deploy.sh)가 끝머리에서 오래된 일자별 로그를 지웁니다. 서버에 따로 크론을 걸 필요가 없습니다.
php spark logs:prune # 몇 개가 지워질지만 본다
php spark logs:prune --force # 실제로 지운다
php spark logs:prune --force --keep-days 14 # 보관 일수 조정- 대상:
writable/logs/log-YYYY-MM-DD.log. 이름이 이 형식이 아니거나 날짜가 유효하지 않은 파일은 건드리지 않습니다. - 기본 보관: 30일 — 오늘 포함 최근 30개 날짜를 남깁니다.
- 되돌릴 수 없는 삭제이므로 기본 동작은 개수 보고이고,
--force를 줘야 지웁니다. - 배포에서는 실패해도 배포를 멈추지 않습니다(백업과 반대 — 로그 정리는 서비스 동작과 무관합니다).
- 배포가 뜸한 환경이라면 크론으로도 돌릴 수 있습니다:
0 4 * * * cd /var/www/ci4blog && sudo -u www-data php spark logs:prune --force
main 에 push 하면 GitHub Actions 가 다음 순서로 움직입니다.
- Test — 전체 스위트를 돌립니다(커버리지 없이). 여기서 실패하면 배포가 시작되지 않습니다.
- Coverage — Test 와 병렬로 커버리지를 측정해 Job Summary 와 HTML 아티팩트를 남깁니다. 이 잡이 실패해도 배포를 막지 않습니다.
- Deploy —
production환경이라 수동 승인이 필요합니다. 승인하면 서버에서scripts/deploy.sh가 돕니다. - Smoke test — 배포 직후 GitHub Actions 러너가 공인 도메인으로 5경로(
/health·/·/posts·/sitemap.xml·/feed)를 확인합니다. SSH 성공은 "스크립트가 끝났다"는 뜻일 뿐이라, 앱이 실제로 응답하는지는 이 단계가 봅니다.
deploy.sh 는 8단계입니다.
| 단계 | 하는 일 |
|---|---|
| 1 | main 최신 코드 받기 |
| 2 | 운영 의존성 설치(--no-dev) |
| 3 | DB 백업 — 실패하면 배포가 그 자리에서 멈춥니다 |
| 4 | 마이그레이션 |
| 5 | 강의 글 발행(posts:import) |
| 6 | 캐시 정리 |
| 7 | 오래된 로그 정리 |
| 8 | writable/ 권한 보정 |
코드를 되돌릴 때는 되돌리는 커밋을 새로 만들어 배포합니다. 서버에서 직접 이전 커밋으로 옮기지 않습니다 — deploy.sh 가 git pull 로 main 을 따라오기 때문에 다음 배포에서 원상복귀합니다.
git revert <되돌릴 커밋>
git push origin main # Test 통과 후 Deploy 를 다시 승인DB 를 되돌려야 하면 위 "운영 — 백업과 복구" 의 복구 절차를 따릅니다. 배포는 마이그레이션 직전에 스냅샷을 만들므로, 그 배포로 생긴 스키마 변경은 되돌릴 백업이 있습니다.
⚠️ 롤백 절차는 아직 실제로 리허설된 적이 없습니다. 위 내용은 배포 파이프라인의 동작에서 도출한 것입니다.
1. 먼저 헬스체크를 봅니다.
curl -s https://<도메인>/health
# {"status":"ok","db":"ok"}db 가 ok 가 아니면 DB 파일 권한이나 경로를 먼저 의심합니다(운영은 SQLite 이고 경로는 .env 의 database.default.database 입니다).
2. 배포가 실패한 경우라면 서버에 들어가기 전에 워크플로 로그를 봅니다. 배포 잡에는 if: failure() 진단 스텝이 있어, 실패 시 최근 애플리케이션 로그와 php-fpm 상태를 자동으로 남깁니다.
3. 애플리케이션 로그는 서버의 writable/logs/log-YYYY-MM-DD.log 입니다. 배포가 30일 지난 것을 지우므로 그 이전 기록은 없습니다.
ls -t /var/www/ci4blog/writable/logs/*.log | head -1 | xargs tail -504. DB 를 되돌려야 하면 위 "운영 — 백업과 복구" 절차를 따릅니다. 백업은 writable/backups/ 에 최근 10개가 있습니다.
ci4blog/
├─ app/
│ ├─ Commands/ # spark 커맨드 (db:backup, db:prune, logs:prune, posts:import)
│ ├─ Config/ # 라우트, 필터, DB, Auth, CSP 설정
│ ├─ Controllers/ # Home, Posts, Comments, Profile, Feed, Health, Pages
│ │ └─ Admin/ # 관리자 화면 (Posts, Comments, Categories)
│ ├─ Database/ # Migrations, Seeds
│ ├─ Entities/ # Post, Comment, Category
│ ├─ Filters/ # SecurityHeaders, RateLimit
│ ├─ Helpers/ # link, acl, db, stats 헬퍼
│ ├─ Libraries/ # RssXml, UploadStorage
│ ├─ Models/ # PostModel, CommentModel, CategoryModel, UserModel
│ └─ Views/ # layouts, partials, home, posts, comments, admin
├─ tests/
│ ├─ Feature/ # 엔드포인트 단위 Feature 테스트
│ └─ unit/ # 스크립트·설정·워크플로 검사
├─ scripts/ # deploy.sh, smoke.sh, coverage-summary.php
├─ content/posts/ # 발행 원고(.md) — 파일 자체는 저장소에 올리지 않습니다
├─ .github/workflows/ # CI/CD (test → coverage → deploy → smoke)
├─ writable/ # 캐시·로그·업로드·DB 백업
└─ public/ # 웹 루트 (index.php)
설계 문서와 커리큘럼은
docs/에 로컬 보관하며 저장소에는 올리지 않습니다.
전체 30회차 · 8개 섹션으로 구성됩니다. 회차별 상세 내용은 태그(git show ep07)와 각 회차의 PR 을 참고하세요.
| 섹션 | 주제 | 회차 | 핵심 내용 |
|---|---|---|---|
| 1 | 스택, 세팅, 구조 | ep01 – ep04 | 프로젝트 생성, 라우팅, 공통 레이아웃, 첫 테스트 |
| 2 | 글 읽기의 기초 | ep05 – ep09 | 마이그레이션·시더, Model/Entity, 목록·페이지네이션·상세 |
| 3 | 인증 (Shield) | ep10 – ep11 | Shield 도입, 인증 필터와 현재 사용자 분기 |
| 4 | 글 작성 CRUD | ep12 – ep17 | 검증·CSRF, 작성·수정·삭제, 플래시, slug URL |
| 5 | 댓글 | ep18 – ep21 | 댓글 표시·저장·삭제, 인증 가드 |
| 6 | 분류와 검색 | ep22 – ep25 | 카테고리 구조·필터, 기본 검색, 상태 유지 |
| 7 | 콘텐츠 풍부하게 | ep26 – ep27 | 마크다운 렌더링, 이미지 업로드·썸네일 |
| 8 | 운영과 유지보수 | ep28 – ep30 | 리팩터링, 버전 업그레이드, production 배포 |
- 한 회차의 파일을 모두 작업한 뒤 한 번에 커밋하고 태그를 답니다.
git commit -m "feat: PostModel/Entity와 글 목록" git tag ep07 - 테스트 회차는 "실패 → 구현 → 통과"를 같은 커밋에 담되, 녹화는 빨강 → 초록 순서로 보여줍니다.
- 커밋 메시지는
feat:,fix:,chore:,refactor:,test:접두사를 사용합니다.
학습 목적의 프로젝트입니다. CodeIgniter 4 프레임워크는 MIT 라이선스를 따릅니다.