CodeIgniter 4로 블로그 만들기 #20 — 댓글 폼과 인증 가드
#지난 회차에서 댓글 저장 백엔드를 테스트 우선으로 완성했습니다. 하지만 아직 브라우저에는 댓글을 입력할 폼이 없습니다. 이번 회차에서 그 폼을 붙입니다.
핵심은 단순히 폼을 그리는 게 아니라, 로그인한 사용자에게만 폼을 노출하고, 비로그인 사용자에게는 로그인 유도를 보여 주는 것입니다. 그리고 이 "폼 노출 가드"는 실무에서 흔히 빠뜨리는 부분이라, 테스트로 못 박고 넘어갑니다.
왜 저장 라우트만 막으면 안 되나
ep19에서 저장 라우트(POST /posts/{id}/comments)를 session 필터로 이미 막았습니다. 그러면 폼 노출까지 신경 쓸 필요가 있을까요?
있습니다. 두 가지는 역할이 다릅니다.
- 저장 라우트 가드(ep19): 비로그인 사용자가 어떻게든 POST를 보내도 서버가 막는다 → 보안.
- 폼 노출 가드(이번 회차): 비로그인 사용자에게 애초에 쓸 수 없는 폼을 보여 주지 않는다 → UX.
폼을 보여 놓고 제출하면 로그인 페이지로 튕기는 건 사용자 입장에서 당황스럽습니다. "로그인해야 쓸 수 있다"는 걸 폼 대신 미리 안내하는 게 좋습니다. 그래서 이 UX 규칙을 코드뿐 아니라 테스트로도 고정합니다.
준비물과 목표
- ep19까지 완성된 저장소 (댓글 저장 백엔드가 동작하는 상태)
- ep11에서 익힌
auth()->loggedIn()뷰 분기
이번에 만들 것:
CommentGuardTest— 비로그인엔 폼 미노출 / 로그인엔 폼 노출 (실패 → 통과)comments/_form.php부분 뷰posts/show.php— 로그인 시 폼 include, 비로그인은 로그인 안내
1. 먼저 실패하는 테스트 (빨강)
이번에도 테스트를 먼저 씁니다. tests/Feature/CommentGuardTest.php입니다.
<?php
namespace Tests\Feature;
use App\Models\PostModel;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Shield\Test\AuthenticationTesting;
/**
* 댓글 폼의 인증 가드에 대한 Feature 테스트.
*
* 댓글 폼은 로그인 사용자에게만 보이고, 비로그인 사용자에게는 보이지 않는다.
* (저장 라우트 자체의 차단은 CommentStoreTest 에서 검증한다.)
*/
final class CommentGuardTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use FeatureTestTrait;
use AuthenticationTesting;
protected $namespace = null;
protected $refresh = true;
protected function setUp(): void
{
parent::setUp();
$_SESSION = [];
\Config\Services::resetSingle('session');
\Config\Services::resetSingle('auth');
}
// ... makeUser(), makePostSlug() 헬퍼 (이전과 동일한 패턴) ...
}
트레이트 구성과 setUp()의 세션 리셋은 이전의 CommentStoreTest와 같은 패턴입니다. 이번엔 상세 페이지를 GET으로 불러 화면에 폼이 있는지를 봐야 하므로, 글의 slug가 필요합니다.
private function makePostSlug(int $userId): string
{
$model = model(PostModel::class);
$model->insert([
'user_id' => $userId,
'title' => '댓글 폼 테스트 글',
'body' => '본문',
]);
return $model->find($model->getInsertID())->slug;
}
그리고 두 시나리오입니다.
public function testGuestDoesNotSeeCommentForm(): void
{
$slug = $this->makePostSlug(1);
$result = $this->call('GET', 'posts/' . $slug);
$result->assertStatus(200);
$result->assertDontSee('댓글 등록');
}
public function testLoggedInUserSeesCommentForm(): void
{
$user = $this->makeUser();
$slug = $this->makePostSlug($user->id);
$result = $this->actingAs($user)->call('GET', 'posts/' . $slug);
$result->assertStatus(200);
$result->assertSee('댓글 등록');
}
포인트:
assertDontSee('댓글 등록')/assertSee('댓글 등록')— 폼의 제출 버튼 라벨("댓글 등록")이 응답 HTML에 있는지로 폼 노출 여부를 판정합니다. 눈에 보이는 문자열을 기준 삼으면 테스트가 읽기 쉽습니다.- 비로그인은
actingAs없이 GET, 로그인은actingAs($user)로 GET. 두 경우 모두 200(상세 페이지 자체는 누구나 볼 수 있음)이지만, 폼 노출만 달라야 합니다.
이 시점엔 폼이 아예 없으니 testLoggedInUserSeesCommentForm이 빨강입니다.
2. 댓글 폼 부분 뷰
로그인 사용자에게 보여 줄 폼을 부분 뷰로 만듭니다. comments/_form.php입니다.
<?php // 댓글 작성 폼 부분 뷰. $post 를 받는다(action URL 용). 로그인 시에만 노출. ?>
<form class="comment-form" action="<?= site_url('posts/' . $post->id . '/comments') ?>" method="post">
<?= csrf_field() ?>
<?php $errors = session('errors') ?? []; ?>
<?php if ($errors !== []): ?>
<ul class="form-errors">
<?php foreach ($errors as $error): ?>
<li><?= esc($error) ?></li>
<?php endforeach ?>
</ul>
<?php endif ?>
<textarea name="body" rows="3" placeholder="댓글을 남겨 보세요"><?= esc(old('body')) ?></textarea>
<button type="submit" class="btn">댓글 등록</button>
</form>
읽어 볼 지점:
action은posts/{post->id}/comments. ep19에서session그룹에 등록한(:num)라우트에 맞춰 글 ID를 씁니다.csrf_field()— CI4의 CSRF 보호. 쓰기 폼에는 반드시 넣습니다(이전부터 이어 온 습관).- 에러 표시 —
session('errors')는 이전 컨트롤러가 검증 실패 시with('errors', ...)로 넘긴 값입니다. 빈 내용을 제출하면 여기에 "댓글 내용을 입력해 주세요."가 찍혀 폼 위에 나타납니다. old('body')— 역시 ep19의withInput()덕분에, 검증 실패로 되돌아왔을 때 방금 친 내용이 textarea에 그대로 남습니다. 사용자가 다시 타이핑하지 않아도 됩니다.esc()로 감싸 XSS도 막습니다.
폼 스타일도 app.css에 조금 더합니다.
.comment-form { margin-top: 24px; }
.comment-form textarea { margin-bottom: 10px; }
.comment-login { margin-top: 24px; color: var(--color-ink-3); }
3. 상세 뷰에서 로그인 분기
이제 상세 뷰(posts/show.php)에서 댓글 목록 아래에 로그인 여부로 분기합니다.
<?php // 댓글 목록(부분 뷰). ?>
<?= $this->include('comments/_list', ['comments' => $comments]) ?>
<?php // 댓글 작성 폼은 로그인 사용자에게만 노출한다(비로그인은 로그인 유도). ?>
<?php if (auth()->loggedIn()): ?>
<?= $this->include('comments/_form', ['post' => $post]) ?>
<?php else: ?>
<p class="comment-login"><a class="nav-link" href="<?= site_url('login') ?>">로그인</a> 후 댓글을 남길 수 있습니다.</p>
<?php endif ?>
auth()->loggedIn()(이전에 헤더 분기에 이미 쓴 그 함수)으로 갈라, 로그인 상태면 폼 부분 뷰를 include하고, 아니면 로그인 링크가 담긴 안내 문구를 보여 줍니다. 폼 부분 뷰에는 action URL을 만들기 위해 $post를 넘깁니다.
이렇게 하면:
- 비로그인: "댓글 등록" 버튼이 렌더링되지 않음 →
assertDontSee('댓글 등록')통과. - 로그인: 폼이 렌더링되어 "댓글 등록" 버튼 등장 →
assertSee('댓글 등록')통과.
확인 — 초록, 그리고 브라우저
테스트를 다시 돌립니다.
composer test -- --filter CommentGuardTest
두 테스트가 초록으로 바뀌면, 브라우저에서도 확인해 봅니다.
php spark serve
- 로그아웃 상태로 글 상세에 가면 댓글 목록 아래에 "로그인 후 댓글을 남길 수 있습니다."가 보입니다.
- 로그인하면 그 자리에 textarea와 "댓글 등록" 버튼이 나타납니다. 실제로 댓글을 써서 등록하면(ep19의 저장 로직이 받습니다) slug 페이지로 돌아오며 방금 단 댓글이 목록에 추가됩니다.
- 내용을 비우고 등록하면 "댓글 내용을 입력해 주세요."가 폼 위에 뜨고, 입력값도 유지됩니다.
여기까지 오면 표시 → 저장 → 폼·가드 가 하나로 이어집니다.
마무리 — 커밋과 태그
이번 회차에서 한 일:
CommentGuardTest— 비로그인 폼 미노출 / 로그인 폼 노출(실패 → 통과)comments/_form.php부분 뷰(CSRF·에러·old()복원) + 폼 스타일posts/show.php— 로그인 시 폼 include, 비로그인은 로그인 안내
git add .
git commit -m "feat: 댓글 폼과 인증 가드"
git tag ep20
다음 회차
이제 댓글을 달 수 있으니, 마지막으로 지울 수 있어야 합니다. 다음 글(#21)에서는 댓글 삭제와 권한을 다룹니다. "댓글 작성자 본인 · 글 작성자 · 관리자만 삭제"라는 권한 조합을, 컨트롤러 가드와 화면 노출 양쪽에서 맞추고, 제3자 차단(403)까지 테스트로 못 박습니다.
이번 회차 요약
- 다루는 파일:
tests/Feature/CommentGuardTest.php·app/Views/comments/_form.php(생성) /app/Views/posts/show.php·public/assets/css/app.css(수정)- 핵심: 저장 라우트 가드(보안)와 폼 노출 가드(UX)는 별개다.
auth()->loggedIn()으로 폼을 로그인 사용자에게만 노출하고, 이 규칙을assertSee/assertDontSee로 고정한다. 폼은 CSRF·old()로 실패 복원까지.
다음: 댓글 삭제와 권한(본인/글작성자/관리자)
댓글 0
아직 댓글이 없습니다.