· nambak80 Blog 로그인
CodeIgniter4

CodeIgniter 4로 블로그 만들기 #11 — 인증 필터와 현재 사용자 분기

CodeIgniter 4로 블로그 만들기 #11 — 인증 필터와 현재 사용자 분기

지난 회차에서 Shield를 설치하고 인증 기반을 깔았습니다. 이제 /login/register가 실제로 동작하고, users 테이블도 준비돼 있습니다. 하지만 화면은 아직 아무것도 달라지지 않았습니다. 로그인한 사용자든 아니든 헤더는 똑같아 보입니다.

이번 회차의 목표는 두 가지입니다. 첫째, 앞으로 만들 쓰기 라우트(글 작성·수정·삭제)를 담을 "로그인 필요" 그룹의 골격을 만듭니다. 둘째, 공통 헤더에서 로그인 상태에 따라 네비게이션을 분기합니다. 이 둘을 합치면 "누가 로그인했고, 그 사람만 무엇을 할 수 있는가"의 뼈대가 잡힙니다.

준비물 / 목표

  • 지난 회차까지 완성된 상태 (Shield 설치·발행·마이그레이션 완료).
  • 이번 회차에서 하는 일:
    • Routes.phpsession 필터 그룹의 빈 골격 추가
    • 헤더에서 auth()->loggedIn()으로 로그인/비로그인 네비 분기
    • 기존 Feature 테스트가 auth를 견디도록 마이그레이션 범위 조정

1. session 필터 그룹 골격

다음 회차부터 글 작성 라우트(posts/new, posts POST)를 추가할 텐데, 이 라우트들은 로그인한 사람만 접근해야 합니다. 라우트마다 개별로 필터를 다는 대신, "로그인이 필요한 라우트를 한 곳에 모으는 그룹"을 미리 만들어 둡니다.

$routes->get('/', 'Home::index');
$routes->get('about', 'Pages::about');
$routes->get('posts', 'Posts::index');

// 로그인(세션 인증)이 필요한 쓰기 라우트는 이 그룹 안에 둔다.
// 글 작성/수정/삭제 라우트가 ep12~ep15에서 여기에 채워진다.
$routes->group('', ['filter' => 'session'], static function ($routes) {
    // (다음 회차부터 추가)
});

$routes->get('posts/(:segment)', 'Posts::show/$1');

service('auth')->routes($routes);

$routes->group('', ['filter' => 'session'], ...)가 핵심입니다.

  • 첫 번째 인자 ''URL 접두어입니다. 빈 문자열이라 그룹 안 라우트의 주소가 그대로(posts/new 등) 유지됩니다.
  • ['filter' => 'session']은 이 그룹 안의 모든 라우트에 session 필터를 적용한다는 뜻입니다. session 필터는 이전 회차에서 Shield가 자동 등록한 별칭으로, "로그인 세션이 없으면 로그인 페이지로 돌려보낸다"를 담당합니다.
  • 지금은 그룹 안이 비어 있습니다. 골격만 먼저 만들어 두고, 라우트는 다음 회차부터 채웁니다. 커밋이 회차별로 깔끔하게 떨어지도록 하는 이 강좌의 방식입니다.

그룹을 posts/(:segment) 상세 라우트보다 위에 둔 것도 의도적입니다. 나중에 posts/new가 이 그룹에 들어가는데, 만약 상세 라우트(posts/(:segment))가 먼저 있으면 new가 "슬러그 new인 글 보기"로 잘못 잡힐 수 있습니다. 위에 두면 posts/new가 먼저 매칭됩니다.

2. 헤더 분기 — 로그인 상태에 따라

이제 공통 헤더(app/Views/partials/header.php)에서 로그인 여부에 따라 다른 메뉴를 보여 줍니다.

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

<span class="nav-spacer"></span>

<?php if (auth()->loggedIn()): ?>
    <span class="nav-user"><?= esc(auth()->user()->username) ?>님</span>
    <a class="nav-link" href="<?= site_url('logout') ?>">로그아웃</a>
<?php else: ?>
    <a class="nav-link" href="<?= site_url('login') ?>">로그인</a>
    <a class="btn btn-ghost" href="<?= site_url('register') ?>">회원가입</a>
<?php endif ?>

여기서 쓴 도구들을 하나씩 봅시다.

  • auth()->loggedIn() — 현재 요청이 로그인 상태인지 true/false로 알려 줍니다. ep10에서 auth 헬퍼를 Autoload에 넣어 뒀기 때문에 별도 로드 없이 바로 씁니다.
  • auth()->user()->username — 로그인한 사용자 객체(Shield의 User 엔티티)에서 사용자명을 꺼냅니다. 사용자가 입력한 값이므로 반드시 esc()로 감싸 XSS를 막습니다.
  • 비로그인 상태에서는 로그인 / 회원가입 링크를, 로그인 상태에서는 사용자명 / 로그아웃을 보여 줍니다.
  • <span class="nav-spacer"></span>는 좌측 메뉴와 우측 인증 메뉴 사이를 벌리는 레이아웃용 조각입니다. 디자인은 ep03의 app.css가 자동으로 입혀 줍니다.

헤더 디자인은 이렇게 회차를 거치며 점진적으로 완성됩니다. 기본 → 글 링크 → 로그인 분기(미구현) → 글쓰기 링크(미구현) 순입니다.

3. 테스트가 auth를 견디게 만들기

여기서 흔히 만나는 함정이 하나 있습니다. 헤더가 이제 모든 페이지에서 auth()->loggedIn()을 호출합니다. 즉, about 같은 정적 페이지를 렌더링할 때도 Shield가 사용자·세션을 조회하려 합니다. 그런데 Feature 테스트 환경에서는 매 테스트마다 DB를 새로 만드는데, 지금까지 테스트는 App 네임스페이스의 마이그레이션(posts 등)만 적용하고 있었습니다. Shield/Settings 테이블이 없으니, 헤더가 auth를 부르는 순간 테스트가 깨집니다.

해결은 간단합니다. 테스트가 적용하는 마이그레이션 네임스페이스를 'App'에서 null(전체)로 넓혀 Shield·Settings 테이블까지 만들게 합니다.

정적 페이지 테스트인 PagesTest는 원래 DB가 필요 없었지만, 이제 DatabaseTestTrait를 추가하고 전체 마이그레이션을 적용하도록 바꿉니다.

final class PagesTest extends CIUnitTestCase
{
    use DatabaseTestTrait;
    use FeatureTestTrait;

    // null 이면 모든 네임스페이스(App·Shield·Settings)의 마이그레이션을 적용한다.
    protected $namespace = null;
    protected $refresh   = true;

PostIndexTestPostShowTest도 이미 있던 protected $namespace = 'App';null로 바꿉니다.

// 모든 네임스페이스의 마이그레이션을 매 테스트마다 새로 적용한다.
// (공통 헤더가 auth()를 호출하므로 Shield/Settings 테이블도 필요하다.)
protected $namespace = null;
protected $refresh   = true;

$namespace = null은 "특정 네임스페이스로 제한하지 말고 모든 마이그레이션을 적용하라"는 뜻이고, $refresh = true는 매 테스트 전에 DB를 새로 리프레시하라는 뜻입니다. 이 조합으로 각 테스트가 깨끗한 상태에서 Shield 테이블까지 갖추고 시작합니다.

핵심 개념 — 그룹 필터 vs. 라우트별 필터

인증이 필요한 라우트마다 ['filter' => 'session']을 개별로 다는 것도 가능하지만, 그룹으로 묶는 편이 훨씬 안전합니다. 이유는 "빠뜨림 방지"입니다. 나중에 라우트가 늘어나도 그룹 안에 넣기만 하면 자동으로 보호되고, "어? 이 라우트는 필터를 안 달았네" 같은 실수가 줄어듭니다. 보안 관련 설정은 한 곳에 모아 두는 것이 원칙입니다.

또 하나, 이번 회차의 테스트 수정은 사소해 보이지만 중요한 교훈을 줍니다. 뷰(헤더)에 로직을 넣으면 그 로직이 모든 페이지의 테스트에 영향을 준다는 점입니다. auth() 호출 하나가 정적 페이지 테스트까지 DB에 의존하게 만들었죠. 공통 레이아웃에 무언가를 넣을 때는 이런 파급 효과를 늘 염두에 둬야 합니다.

마무리 — 커밋과 태그

이번 회차에서 한 일:

  • Routes.phpsession 필터 그룹 골격 추가(내용은 다음 회차부터)
  • 헤더에서 auth()->loggedIn()으로 로그인/비로그인 네비 분기
  • PagesTest·PostIndexTest·PostShowTest의 마이그레이션 네임스페이스를 null로 확장
git add .
git commit -m "feat: 인증 필터와 현재 사용자 분기"
git tag ep11

다음 회차

다음 글부터는 드디어 글 작성입니다. 먼저 백엔드(저장 + 검증)를 **테스트 우선(TDD)**으로 만듭니다. "게스트 차단 → 저장 성공 → 검증 실패"를 검증하는 실패 테스트를 먼저 짜고, 컨트롤러의 create()와 모델의 검증 규칙으로 초록불을 켭니다. 검증 실패 시 입력값을 살리는 withInput()도 이때 등장합니다.


이번 회차 요약

  • 다루는 파일: app/Config/Routes.php(수정) / app/Views/partials/header.php(수정) / tests/Feature/PagesTest.php·PostIndexTest.php·PostShowTest.php(수정)
  • 핵심: 쓰기 라우트는 ['filter' => 'session'] 그룹으로 묶어 한 곳에서 보호한다. 헤더는 auth()->loggedIn()·auth()->user()로 분기하고, 사용자 입력은 esc()로 감싼다. 헤더가 auth를 부르면서 정적 페이지 테스트까지 Shield 테이블이 필요해져 마이그레이션 범위를 넓힌다.

다음: 글 저장 백엔드와 검증

댓글 0

아직 댓글이 없습니다.

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

← 목록으로