· nambak80 Blog 로그인
CodeIgniter4

CodeIgniter 4로 블로그 만들기 #03 — 공통 레이아웃과 블로그 골격

CodeIgniter 4로 블로그 만들기 #03 — 공통 레이아웃과 블로그 골격

지난 회차에서 about 페이지 하나를 만들면서 라우트 → 컨트롤러 → 뷰 흐름을 익혔습니다. 그런데 그때 뷰(pages/about.php)에는 <!DOCTYPE html>부터 </html>까지 문서 전체가 들어 있었죠. 페이지를 하나 더 만들 때마다 이 뼈대를 통째로 복사해야 하는 상태입니다.

이번 회차의 목표는 두 가지입니다.

  1. 그 중복을 공통 레이아웃으로 걷어내기 — CI4의 extend() / section() / renderSection()
  2. 블로그의 첫 디자인을 순수 CSS 한 장으로 입히기 — 헤더·푸터, 색·폰트·컴포넌트 토큰

이 회차가 끝나면 이후 모든 화면(글 목록, 상세, 작성 폼…)은 이 레이아웃과 CSS를 그대로 물려받아 그려집니다. 즉 지금 한 번 잘 깔아 두면 나중엔 본문만 채우면 되는 구조가 됩니다.

이번 회차 목표

  • 공통 레이아웃(layouts/default) 만들고 뷰 상속 구조 이해하기
  • 헤더·푸터를 별도 조각(partial)으로 분리하기
  • about 페이지를 레이아웃 상속 방식으로 리팩터링하기
  • 디자인 토큰(색·폰트)과 공통 컴포넌트를 담은 app.css 도입하기

1. 공통 레이아웃 만들기

CI4는 뷰끼리 상속을 지원합니다. "공통 뼈대(레이아웃)를 한 번 정의하고, 각 페이지는 본문만 끼워 넣는" 방식입니다. 먼저 뼈대가 될 app/Views/layouts/default.php를 만듭니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $this->renderSection('title') ?> · CI4 Blog</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap">
    <link rel="stylesheet" href="<?= base_url('assets/css/app.css') ?>">
</head>
<body>
    <?= $this->include('partials/header') ?>

    <main class="container">
        <?= $this->renderSection('content') ?>
    </main>

    <?= $this->include('partials/footer') ?>
</body>
</html>

여기에 세 가지 새로운 장치가 등장합니다.

  • renderSection('title') / renderSection('content') — 각 페이지가 채워 넣을 "구멍"입니다. 자식 뷰가 section('title') … endSection()으로 채운 내용이 여기에 꽂힙니다. 제목은 <title> 태그 안에, 본문은 <main> 안에 들어갑니다.
  • include('partials/header') / include('partials/footer') — 다른 뷰 조각을 그 자리에 끼워 넣습니다. 헤더/푸터처럼 모든 페이지가 공유하는 부분을 별도 파일로 빼 두고 불러오는 것입니다.
  • <head>의 링크 세 줄 — 웹 폰트 두 개(본문용 Pretendard, 코드/날짜용 JetBrains Mono)를 CDN으로 불러오고, 우리 스타일시트 app.css를 연결합니다. base_url('assets/css/app.css')app.baseURL을 기준으로 전체 경로를 만들어 줍니다(그래서 ep01에서 .envapp.baseURL을 맞춰 둔 것이 여기서 쓰입니다).

<title>을 잘 보면 <?= $this->renderSection('title') ?> · CI4 Blog입니다. 즉 각 페이지는 앞부분만 채우고 뒤의 · CI4 Blog는 사이트 공통으로 붙습니다.

2. 헤더·푸터 조각 만들기

레이아웃이 include로 부르는 두 조각을 만듭니다. 이런 부분 뷰는 partials/ 폴더에 둡니다.

먼저 app/Views/partials/header.php:

<header class="site-header">
    <nav class="nav container">
        <a class="brand" href="<?= site_url('/') ?>"><span class="dot">·</span> 지환의 노트</a>
        <a class="nav-link" href="<?= site_url('about') ?>">소개</a>
    </nav>
</header>

블로그 이름은 "지환의 노트"입니다. 로고(.brand) 앞의 점(·)에는 포인트 색을 주려고 <span class="dot">으로 감쌌습니다. site_url()base_url()과 비슷하게 앱 기준 주소로 링크를 만들어 줍니다. 지금 네비게이션 링크는 "소개" 하나뿐이지만, 이 헤더는 앞으로 회차를 거치며 점점 완성됩니다.

그리고 app/Views/partials/footer.php:

<footer class="site-footer">
    <div class="container">
        <p>&copy; <?= date('Y') ?> 지환의 노트 · CodeIgniter 4 학습 프로젝트</p>
    </div>
</footer>

date('Y')로 올해 연도를 자동으로 넣었습니다.

3. about 페이지를 레이아웃 상속으로 전환

이제 ep02에서 문서 전체를 담고 있던 app/Views/pages/about.php를 확 줄입니다. 뼈대는 레이아웃이 갖고 있으니, 이 페이지는 채울 내용(제목·본문)만 남깁니다.

<?= $this->extend('layouts/default') ?>

<?= $this->section('title') ?>소개<?= $this->endSection() ?>

<?= $this->section('content') ?>
    <h1 class="page-title">소개</h1>
    <p>CodeIgniter 4로 한 회차씩 만들어 가는 학습용 블로그입니다.</p>
    <p>이 페이지는 공통 레이아웃 위에 본문만 끼워 넣어 그려집니다.</p>
<?= $this->endSection() ?>

동작 방식은 이렇습니다.

  • extend('layouts/default') — "나는 이 레이아웃을 뼈대로 쓴다"는 선언. 파일 맨 위에 옵니다.
  • section('title') … endSection() — 레이아웃의 renderSection('title') 자리에 꽂힐 내용. 여기서는 "소개".
  • section('content') … endSection() — 레이아웃의 renderSection('content') 자리, 즉 <main> 안에 꽂힐 본문.

<html><head><body>도 사라졌습니다. 이 페이지는 이제 "제목은 소개, 본문은 이것"이라는 정보만 담고 있고, 나머지 껍데기는 레이아웃이 씌워 줍니다. ep02에서 일부러 만들어 둔 중복을 여기서 걷어낸 셈입니다. 앞으로 페이지를 하나 더 만들 때는 이 세 조각(extend, title 섹션, content 섹션)만 쓰면 됩니다.

제목에 붙은 .page-title, 이후 화면에서 쓸 카드·버튼 같은 클래스들의 실제 모양은 다음의 app.css가 담당합니다.

4. 디자인을 순수 CSS 한 장으로

이 강좌의 디자인은 빌드 도구 없이 순수 CSS 한 파일로만 적용합니다. Tailwind나 Sass 컴파일 같은 별도 단계 없이, public/assets/css/app.css 하나면 됩니다. public/ 아래에 두는 이유는 ep01에서 봤듯 웹 서버가 실제로 바라보는 곳이 public/이기 때문입니다.

이 파일은 크게 두 부분으로 구성됩니다.

(1) 디자인 토큰 — :root 변수

색·폰트·둥근 모서리 같은 값을 CSS 변수로 한 곳에 모읍니다.

:root {
  --color-accent: #5E81AC; --color-accent-bright: #88C0D0; --color-accent-soft: #E3EBF3;
  --color-ink: #2E3440; --color-ink-2: #434C5E; --color-ink-3: #4C566A; --color-ink-4: #9AA5B5;
  --color-paper: #ffffff; --color-bg: #E5E9F0; --color-paper-warm: #ECEFF4;
  --color-line: #D8DEE9; --color-line-soft: #E9EDF3;
  --color-success: #A3BE8C; --color-danger: #BF616A; --color-caution: #EBCB8B;
  --color-code-bg: #2E3440; --color-prose: #27272a;
  --font-sans: 'Pretendard Variable', Pretendard, -apple-system, system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
  --radius: 6px; --radius-lg: 12px; --radius-pill: 999px;
}

차분한 청회색 계열(Nord 팔레트에 가까운 톤)입니다. --color-accent가 포인트 색, --color-ink*는 진하기가 다른 글자색, --color-bg는 배경, --color-paper는 카드 배경입니다. 이렇게 값에 이름을 붙여 두면 나중에 색을 바꿀 때 이 한 곳만 고치면 사이트 전체가 따라 바뀝니다. 폰트도 <head>에서 CDN으로 불러온 Pretendard(본문)와 JetBrains Mono(코드·날짜)를 변수로 묶어 둡니다.

(2) 공통 컴포넌트

토큰 위에서, 앞으로 계속 재사용할 UI 조각들의 스타일을 미리 정의합니다. 지금 화면(about)에서는 헤더·푸터·.page-title 정도만 쓰이지만, 이후 회차를 대비해 컴포넌트를 함께 넣어 둡니다.

  • 레이아웃: .container(가운데 정렬, 최대 960px), body 기본 배경·폰트, .site-header / .site-footer
  • 네비게이션: .brand(로고), .nav-link, 그리고 뒤에서 쓸 .nav-spacer / .nav-user
  • 버튼: .btn(기본, 알약 모양), .btn-ghost(테두리형), .btn-danger(삭제용)
  • 칩/뱃지: .chip, .badge — 카테고리·상태 표시용(뒤 회차)
  • 제목: .page-title(목록·소개 페이지 제목)
  • 글 목록: .post-list(2열 카드 그리드, 좁은 화면에선 1열) — ep07의 글 목록이 입을 스타일
  • 글 상세: .post, .post-body, .post-actions — ep09의 상세 화면용
  • 본문(prose): .prose 계열 — ep26 마크다운 렌더링 대비
  • : .form, input/textarea, .form-errors — 글 작성/수정 폼(ep12~)용
  • 페이지네이션: .pagination — CI4 기본 페이저 마크업(nav > ul.pagination)에 맞춘 스타일(ep08)

예를 들어 버튼과 글 목록 카드는 이렇게 정의돼 있습니다.

/* buttons */
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px;
  border-radius: var(--radius-pill); background: var(--color-accent); color: #fff;
  font-size: 13px; font-weight: 500; border: none; cursor: pointer; text-decoration: none; }
.btn-ghost { background: transparent; color: var(--color-ink); border: 1px solid var(--color-line); }
.btn-danger { background: var(--color-danger); }

/* post list (card grid) */
.post-list { list-style: none; padding: 0; margin: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }
.post-list li { background: var(--color-paper); border: 1px solid var(--color-line);
  border-radius: var(--radius-lg); padding: 22px; }
@media (max-width: 640px) { .post-list { grid-template-columns: 1fr; } }

지금 당장 화면에 안 보이는 클래스까지 미리 넣는 게 이상해 보일 수 있습니다. 하지만 이 강좌의 디자인 전략이 바로 그겁니다. "마크업은 뷰에서 시맨틱한 클래스 이름만 붙이고, 그 클래스의 실제 모양은 app.css가 이미 알고 있다." 덕분에 이후 회차에서는 기능(컨트롤러·모델)에 집중하고, 뷰에는 class="post-list" 같은 이름만 달면 디자인이 자동으로 입혀집니다.

5. 브라우저에서 확인

개발 서버에서 다시 열어 봅니다.

http://localhost:8080/about

이번엔 밋밋한 검정 글씨가 아니라, 상단에 "· 지환의 노트" 헤더와 "소개" 링크, 가운데 정렬된 본문, 하단 푸터까지 갖춘 모습이 보입니다. 폰트도 Pretendard로 바뀌어 있습니다. 그리고 홈(/)이나 about이나 헤더·푸터는 동일하게 나옵니다 — 레이아웃을 공유하고 있으니까요.

CSS가 안 먹는다면 대개 app.baseURL이 개발 서버 주소와 다르거나, public/assets/css/app.css 경로가 틀린 경우입니다. 브라우저 개발자 도구의 네트워크 탭에서 app.css 요청이 200인지 확인해 보세요.

핵심 개념 — 레이아웃 상속과 "디자인은 CSS 한 장"

이번 회차에서 잡은 두 가지 습관은 끝까지 갑니다.

  • 레이아웃 상속(extend/section/renderSection) — 페이지는 "무엇을 보여줄지"만 말하고, "어떤 껍데기를 쓸지"는 레이아웃이 책임집니다. 헤더에 메뉴 하나를 추가하면 모든 페이지에 한 번에 반영됩니다.
  • 빌드 없는 순수 CSS 토큰/컴포넌트 — 색·폰트를 변수로, 반복되는 UI를 클래스로 미리 정리해 두었습니다. 이후 회차의 뷰는 이 어휘(.post-list, .btn, .page-title…)를 그대로 쓰기만 하면 됩니다.

마무리 — 커밋과 태그

이번 회차에서 한 일:

  • layouts/default 공통 레이아웃 도입(title/content 섹션, header/footer include)
  • partials/header, partials/footer 조각 분리
  • about 페이지를 extend('layouts/default')로 리팩터링(중복 HTML 제거)
  • 디자인 토큰과 공통 컴포넌트를 담은 app.css 추가

한 번에 커밋하고 태그를 답니다.

git add .
git commit -m "feat: 공통 레이아웃과 블로그 골격"
git tag ep03

다음 회차

다음 글에서는 테스트 환경을 점검하고 첫 테스트를 작성합니다. appstarter에 이미 준비된 SQLite 메모리 테스트 그룹과 PHPUnit 설정을 확인하고, FeatureTestTrait::call()로 방금 만든 about 페이지가 200으로 응답하는지, 제목이 제대로 렌더링되는지 자동으로 검증합니다.


이번 회차 요약

  • 다루는 파일: app/Views/layouts/default.php(추가) / app/Views/partials/header.php·footer.php(추가) / public/assets/css/app.css(추가) / app/Views/pages/about.php(수정)
  • 핵심: extend()/section()/renderSection()로 공통 레이아웃을 만들어 중복 HTML을 제거한다. 디자인은 빌드 없이 순수 CSS 한 장(:root 토큰 + 공통 컴포넌트)으로 적용하고, 이후 회차의 화면은 이 어휘를 그대로 입는다.

다음: 테스트 환경 점검과 첫 테스트

댓글 0

아직 댓글이 없습니다.

로그인 후 댓글을 남길 수 있습니다.

← 목록으로