Skip to content

Repository files navigation

ci4blog

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

배포 (production)

운영 환경은 개발과 두 가지가 다릅니다 — 디버그를 끄고(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

체크리스트

  • .envCI_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/ 권한

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 를 쓰는 환경입니다(다른 드라이버에서는 커맨드가 건너뜁니다). 즉 .envdatabase.default.DBDriver = SQLite3 이고, database.default.databaseDB 파일 경로입니다 — 아래 절차의 대상이 그 파일입니다.

  1. 웹 서버/PHP-FPM 을 멈춥니다.
  2. 현재 DB 파일(.envdatabase.default.database 경로)을 다른 이름으로 옮겨 둡니다 — 원인 조사에 필요합니다.
  3. 되돌릴 백업 파일을 그 경로로 복사합니다.
  4. 파일 소유자를 PHP-FPM 실행 사용자로 맞춥니다(chown).
  5. 서비스를 다시 올리고 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 가 다음 순서로 움직입니다.

  1. Test — 전체 스위트를 돌립니다(커버리지 없이). 여기서 실패하면 배포가 시작되지 않습니다.
  2. Coverage — Test 와 병렬로 커버리지를 측정해 Job Summary 와 HTML 아티팩트를 남깁니다. 이 잡이 실패해도 배포를 막지 않습니다.
  3. Deployproduction 환경이라 수동 승인이 필요합니다. 승인하면 서버에서 scripts/deploy.sh 가 돕니다.
  4. 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.shgit pullmain 을 따라오기 때문에 다음 배포에서 원상복귀합니다.

git revert <되돌릴 커밋>
git push origin main          # Test 통과 후 Deploy 를 다시 승인

DB 를 되돌려야 하면 위 "운영 — 백업과 복구" 의 복구 절차를 따릅니다. 배포는 마이그레이션 직전에 스냅샷을 만들므로, 그 배포로 생긴 스키마 변경은 되돌릴 백업이 있습니다.

⚠️ 롤백 절차는 아직 실제로 리허설된 적이 없습니다. 위 내용은 배포 파이프라인의 동작에서 도출한 것입니다.

운영 — 장애 대응

1. 먼저 헬스체크를 봅니다.

curl -s https://<도메인>/health
# {"status":"ok","db":"ok"}

dbok 가 아니면 DB 파일 권한이나 경로를 먼저 의심합니다(운영은 SQLite 이고 경로는 .envdatabase.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 -50

4. 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 라이선스를 따릅니다.

About

CodeIgniter4 프레임워크를 이용한 블로그

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages