이번 챕터의 코드는 https://github.com/koeunyeon/ci4/commits/blog-connect-post-member 에 있습니다.
기능이 거의 다 만들어졌습니다. 이제 글 기능에 회원 기능을 녹여넣으면 MVP는 완성입니다.
수정해야 할 목록을 리스트업 해 보겠습니다.
엄청 많아서 귀찮을 것 같지만 대부분 반복되는 코드이기 때문에 사실 간단하게 복사 붙여넣기로 처리 가능합니다.
코드이그나이터 4.0.4 버전에서는 파일 세션 관련 버그가 있는 것 같습니다. 만약 글로벌 세션 $_SESSION
에 값이 있을 때 session()
헬퍼 함수로 세션을 시작하면, $_SESSION
안의 값들 중 키가 ci_
로 시작하는 것들을 제외하고는 모두 없에버리더군요.
아마 내부적으로 코드이그나이터 세션 키는 ci_session
을 이용하고 PHP는 PHPSESSID
를 사용하므로 덮어쓰는게 아닐 까 생각합니다만, 우리가 원하는 동작은 이게 아니죠.
우리는 소셜 로그인을 사용할 때 부득이하게 글로벌 세션을 사용했으므로 글로벌 세션에 접근할 방법이 필요합니다. 따라서 글로벌 세션에 접근하는 헬퍼 클래스를 만들겠습니다.
app/Helpers/SessionGlobalHelper.php
<?php
namespace App\helpers;
class SessionGlobalHelper
{
public static function start() // (1)
{
if (isset($_SESSION) == false) { // (2)
session_start();
}
}
public static function set($key, $val) // (3)
{
self::start();
$_SESSION[$key] = $val;
}
public static function get($key, $default = null) // (4)
{
self::start();
if (isset($_SESSION[$key])) {
return $_SESSION[$key];
}
return $default;
}
public static function remove($key) // (5)
{
self::start();
if (isset($_SESSION[$key])) {
unset($_SESSION[$key]);
}
}
}
위 코드는 POPL - https://github.com/poplcode/popl 프로젝트의 세션 라이브러리를 가져와서 클래스 형식으로 수정한 것입니다.
꼭 코드이그나이터4의 문제가 아니라 어떤 언어의 어떤 프레임워크를 쓰더라도 이런 라이브러리 충돌 문제는 생깁니다. 프레임워크는 80%의 문제를 쉽게 해결할 수 있게 해 주지만, 나머지 20%의 프레임워크 문제는 80%의 문제를 쉽게 해결해서 아낀 시간만큼을 허비하게 하죠.
이런 20%의 문제를 해결하기 위해 특정 프레임워크를 쓰기 위해서는 그 프레임워크의 전문가가 필요해집니다.
(1) 세션을 시작합니다.
PHP는 여러군데에서 require
되는 특성상 어느 파일에서 세션을 시작할 지 모릅니다. 코드이그나이터4 안에서야 전체 요청 흐름을 제어할 수 있겠지만 외부 라이브러리를 쓰는 순간 정상 작동하지 않을 가능성이 큽니다.
PHP에는 슈퍼 글로벌이라고 불리는 어떤 영역에도 속하지 않고 어디서든 접근할 수 있는 변수들이 존재하기 때문에, 슈퍼 글로벌 변수들에 대해서는 늘 검사가 필요합니다.
static
이 메소드에 사용되면 정적 메소드라는 뜻입니다. 정적 메소드는 인스턴스의 메소드가 아니라 클래스의 메소드여서 클래스의 인스턴스를 생성하지 않고 곧바로 쓸 수 있습니다. 개인적으로는 멤버 변수가 없는(즉, 멤버 메소드만 있는) 인스턴스들는 모두 같은 상태를 가지므로 굳이 인스턴스화 할 필요가 없다고 생각해서 static method
로 선언했습니다.
(2) 세션이 시작되었는지 검사하는 가장 간단한 방법은 $_SESSION
값이 있는지 검사하는 겁니다. 세션이 시작하면 슈퍼 글로벌 변수 $_SESSION
이 생기죠.
(3) 세션에 값을 넣습니다.
(4) 세션에서 값을 가져옵니다. 값이 없을 경우 $default
의 값을 가져옵니다. $default
의 기본값은 null
이기 때문에, 값이 없고 두번째 파라미터가 없다면 null
을 리턴합니다.
(5) 세션에서 값을 삭제합니다.
세션 헬퍼를 이용해 로그인 유무를 확인할 수 있는 헬퍼 함수를 만듭니다. 로그인 여부는 전역적으로 사용되기 때문에 간단한 함수를 만들어 두면 사용하기 편리합니다.
app/Helpers/LoginHelper.php
<?php
namespace App\helpers;
class LoginHelper
{
public const MEMBER_ID = "member_id"; // (1)
public static function isLogin() // (2)
{
return SessionGlobalHelper::get(self::MEMBER_ID) !== null; //(3)
}
public static function memberId(){ // (4)
return SessionGlobalHelper::get(self::MEMBER_ID);
}
}
(1) 로그인 여부를 확인하는 세션의 키가 member_id
라는 것을 상수로 선언했습니다. 이렇게 선언해 두면 나중에 사용자를 식별하는 키가 변경되어야 할 때 한군데만 고치면 됩니다.
(2) 로그인되어 있는지 확인하는 정적 메소드입니다.
(3) 만약 글로벌 세션에 member_id
가 있다면 로그인 된 것으로 판단합니다.
(4) 회원 정보를 가져옵니다. 세션에 있는 값을 바로 빼내도 될 텐데 굳이 메소드를 분리하는 이유는 이 편이 훨씬 코드를 읽기 쉽기 때문입니다. 코드를 사용하는 입장에서는 굳이 사용자를 식별하는 세션 키가 member_id
라는 것을 알 필요가 없어지므로 한 층 추상화가 이루어집니다. 이정도 추상화는 구현하는 시간에 그닥 차이가 없으므로 일정에 영향을 주지 않는 수준으로만 추상화합시다.
생성 페이지는 로그인한 사람만 접근 가능하게 수정하겠습니다. Post
컨트롤러의 create()
메소드를 수정합니다.
app/Controllers/Post.php
public function create()
{
if (LoginHelper::isLogin() === false) { // (1)
return $this->response->redirect("/post");
}
if ($this->request->getMethod() === "get") {
return view("/post/create");
}
... (생략)...
}
(1) 메소드가 시작할 때 로그인 여부를 검사하고 로그인이 되어 있지 않으면 글 목록 페이지로 이동합니다. 이 기능은 GET, POST 둘 다 적용됩니다.
조건식을 보면 ! LoginHelper::isLogin()
형태가 아니라 LoginHelper::isLogin() === false
형태로 되어 있는 것을 볼 수 있는데요. 저는 개인적으로 코드를 읽다가 !
표시를 놓치고 코드를 잘못 해석한 일이 많아서 === false
형식으로 사용하는 것을 좋아합니다. 이 부분은 기호에 따라 다르니 편하실 대로 쓰시면 됩니다.
만약 if (LoginHelper::isLogin()
코드 중 LoginHelper
아래에 빨간 지렁이줄이 간다면 LoginHelper
에 커서를 두고 Ctrl + Space
를 눌러서 자동으로 use
문을 만들어내세요. 보통 PHPStorm은 자동으로 호출되는 코드를 넣어주지만, 어떤 오류로 인해 자동으로 use
문이 채워지지 않는다면 클래스 선언 위에 아래의 선언문을 넣어주시면 됩니다.
use App\helpers\LoginHelper;
우리가 지금 테스트하는 브라우저는 이미 "소셜 로그인된 상태"일 겁니다. 따라서 로그인이 안되어 있을 때의 테스트가 어려울 거에요. 그래서 우리는 브라우저의 시크릿 모드를 이용하겠습니다.
크롬, 엣지에서는 ctrl + shift + n
키를, 파이어폭스에서는 ctrl + shift + p
키를 누릅니다.
시크릿 모드로 띄운 창에서 글 생성 페이지 - localhost:8080/post/create를 접속해 봅시다. 글 목록 페이지로 튕겨나옵니다.
글을 데이터베이스에 생성하는 기존 코드는 아래와 같았습니다.
app/Controllers/Post.php
$data = $this->add_input_markdown();
$post_id = $model->insert($data);
변경할 코드는 아래와 같습니다. 입력값을 그대로 넣는 것 외에 a.) 마크다운으로 변환한 html_content
를 add_input_markdown
으로 설정하고, b.) 세션에서 가져온 회원값 또한 데이터베이스에 넣게 됩니다.
$data = $this->add_input_markdown(); // (1)
$data['author'] = LoginHelper::memberId(); // (2)
$post_id = $model->insert($insert_data); // (3)
(1) 마크다운으로 변환된 글을 가져오는 기능은 동일합니다.
(2) 작가의 정보를 배열에 담습니다.
(3) 데이터를 저장합니다.
만들었으면 확인해 봐야 합니다. 당연히 될 것이라고 생각하는 믿음이 버그를 만듭니다.
http://localhost:8080/post/create
확인도 해 봐야죠.
수정 페이지의 로그인 검사는 생성 페이지의 로그인 검사와 완전히 동일하기 때문에 복사 붙여넣기 하겠습니다.
app/Controllers/Post.php
public function edit($post_id)
{
if (LoginHelper::isLogin() === false) { // (1)
return $this->response->redirect("/post");
}
수정 페이지는 글을 쓴 사람만 수정할 수 있기 때문에, 자기가 쓴 글인지도 검사해야 합니다.
Post.php
파일의 edit
메소드에 아래의 코드를 추가합니다.
app/Controllers/Post.php
$post = $model->find($post_id);
if (!$post) {
return $this->response->redirect("/post");
}
// 추가된 코드
if ($post['author'] !== LoginHelper::memberId()){ // (1)
return $this->response->redirect("/post");
}
// 추가된 코드 끝
if ($this->request->getMethod() === "get") {
(1) 글쓴이($post['author']
)가 로그인한 사용자와 다르다면 글 목록으로 리다이렉트합니다. 이미 로그인 여부와 글이 있는지 여부는 확인했기 때문에 null
검사를 하지 않고 로직을 검증할 수 있습니다.
처음에 샘플로 넣은 글(1번 글)은 아직 저자 정보를 작성하지 않았기 때문에, author
컬럼이 비어있습니다. 이 점을 이용해서, 정말 자기가 쓴 글일 때만 수정 페이지를 접속할 수 있는지 확인해 봅시다.
글 수정 페이지 - localhost:8080/post/edit/1에 접속해 보면, 자동으로 글 목록 페이지 - http://localhost:8080/post로 튕겨나갑니다.
삭제 페이지도 수정 페이지와 동일하게 수정하겠습니다. 먼저 로그인 여부를 확인합니다.
app/Controllers/Post.php
public function delete()
{
// 추가된 부분 (1) 시작
if (LoginHelper::isLogin() === false) { // (1)
return $this->response->redirect("/post");
}
// 추가된 부분 (1) 끝
글쓴이인지 확인합니다.
if (!$post) {
return $this->response->redirect("/post");
}
// 추가된 코드 (2) 시작
if ($post['author'] !== LoginHelper::memberId()){
return $this->response->redirect("/post");
}
// 추가된 코드 끝
컨트롤러에서 뷰에 데이터를 전달할 때 글쓴이인지 정보를 함께 전달해 보겠습니다. 기존의 show
메소드는 아래와 같은 값을 리턴했습니다.
app/Controllers/Post.php
return view('/post/show',[
'post' => $post
]);
글쓴이 정보도 함께 뷰로 전달하도록 수정합시다.
$isAuthor = LoginHelper::isLogin() && $post['author'] == LoginHelper::memberId(); // (1)
return view('/post/show',[
'post' => $post,
'isAuthor' => $isAuthor
]);
(1) 조회 페이지는 로그인 여부를 사전에 확인하지 않기 때문에 로그인 되어 있고(LoginHelper::isLogin()
) 글쓴이라면 $post['author'] == LoginHelper::memberId()
글을 수정/삭제할 권한이 있다고 판단합니다.
이번에는 뷰를 수정해서 수정/삭제 버튼이 보이게 해 보겠습니다.
기존의 컨텐츠 영역 아래에 버튼 부분을 추가합니다.
app/Views/post/show.php
<!-- 신규 추가 영역 시작 -->
<?php if ($isAuthor) : // (1) ?>
<div style="text-align: right;">
<form method="POST" action="/post/delete"> <!-- (2) -->
<a href="/post/edit/<?= $post['post_id'] ?>" class="btn btn-info">수정</a> <!-- (3) -->
<input type="hidden" name="post_id" value="<?= $post['post_id']?>" /> <!-- (4) -->
<input type="submit" class="btn btn-danger" value="삭제"> <!-- (5) -->
</form>
</div>
<?php endif ?>
<!-- 신규 추가 영역 끝 -->
(1) 글쓴이에게만 보이는 영역을 시작합니다.
(2) 우리는 삭제 엔드포인트 /post/delete 를 HTTP POST 메소드만 받아들이게 작성했습니다. 따라서 <form>
으로 감싸서 HTTP POST로 데이터를 전송하도록 합니다.
(3) 글 수정 링크입니다. 단순 a
태그이므로 폼 값이 서버로 전송되지 않습니다.
(4) input type="hidden"
태그는 사용자에게는 보이지 않지만 프로그램을 위해서는 존재해야 하는 값을 다룰 때 쓰입니다. 주로 예제처럼 POST로 값을 전달하거나 할 때죠. 웹 브라우저 화면에는 아무런 표시도 나오지 않습니다만, <form>
태그 안에 있다면 서버에서는 값을 읽을 수 있습니다.
(5) 서버로 데이터를 전송하는 버튼입니다.
똑같이 조회 페이지 - http://localhost:8080/index.php/post/show/3에 접속했을 때 로그인이 되어 있는 브라우저와 로그인이 되어 있지 않은 브라우저는 서로 다른 화면이 보여집니다.
두 이미지 중 위의 이미지는 로그인이 되어 있고 글 작성자일 때 버튼이 보여지는 것을 나타내고 아래 이미지는 로그인이 되어 있지 않았을 때의 모양을 나타냅니다.
뷰의 저장 버튼 아래에 취소 버튼을 추가하겠습니다. 생성과 수정은 같은 뷰를 사용하므로 하나만 수정하면 됩니다.
app/Views/post/create.php
<input type="submit" class="btn btn-primary" value="저장">
<a href="/post/" class="btn btn-info">취소</a> <!-- 추가된 코드 -->
목록 컨트롤러에서 로그인 여부를 뷰로 전달합니다.
app/Controllers/Post.php
return view("post/index", [
'post_list' => $post_list,
'pager' => $pager,
'isLogin' => LoginHelper::isLogin() // 추가된 코드
]);
뷰에 글쓰기 버튼을 추가합니다.
app/Views/post/index.php
<?= $pager->links() ?>
<!-- 추가된 코드 시작 -->
<?php if ($isLogin) : ?>
<p style="text-align: right;">
<a href="/post/create" class="btn btn-primary">글쓰기</a>
</p>
<?php endif ?>
<!-- 추가된 코드 끝 -->
<?= $this->endSection() ?>
원칙적으로는 뷰에서 사용되는 데이터는 모두 컨트롤러에서 전달되어야 합니다. 하지만 레이아웃은 모든 페이지에서 사용되므로, 로그인 여부를 뷰에 전달하기 위해 모든 페이지의 컨트롤러를 수정하는 건 너무 귀찮은 일이죠.
그래서 레이아웃에서는 직접 로그인 여부에 접근하도록 하겠습니다.
기존의 네비게이션 영역은 아래와 같았습니다.
app/Views/post/layout.php
<ul class="navbar-nav flex-column text-left">
<li class="nav-item active">
<a class="nav-link" href="index.html"><i class="fas fa-home fa-fw mr-2"></i>Blog Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="blog-post.html"><i class="fas fa-bookmark fa-fw mr-2"></i>Blog Post</a>
</li>
</ul>
아래와 같이 변경하겠습니다.
<ul class="navbar-nav flex-column text-left">
<li class="nav-item">
<?php
if (App\helpers\LoginHelper::isLogin()) {
$login_link = "/oauth/logout";
$login_message = "로그아웃";
}else{
$login_link = "/oauth/google";
$login_message = "로그인";
}
?>
<a class="nav-link" href="<?= $login_link ?>"><i class="fas fa-bookmark fa-fw mr-2"></i><?= $login_message ?></a>
</li>
</ul>
이제 네비게이션 링크는 로그인 유무를 나타내도록 바뀌었습니다.
블로그 이름을 바꾸고 링크를 글 목록으로 변경하겠습니다. 기존 코드는 아래와 같습니다.
app/Views/post/layout.php
<h1 class="blog-name pt-lg-4 mb-0"><a href="index.html">Anthony's Blog</a></h1>
이렇게 바꿀께요.
<h1 class="blog-name pt-lg-4 mb-0"><a href="/post">마크다운 블로그</a></h1>
이미지와 설명은 간결함을 위해 제거하겠습니다. 아래의 코드를 삭제해 주세요.
app/Views/post/layout.php
<img class="profile-image mb-3 rounded-circle mx-auto" src="/assets/images/profile.png" alt="image" >
<div class="bio mb-3">Hi, my name is Anthony Doe. Briefly introduce yourself here. You can also provide a link to the about page.<br><a href="about.html">Find out more about me</a></div><!--//bio-->
Get in Touch 영역도 지울께요.
app/Views/post/layout.php
<div class="my-2 my-md-3">
<a class="btn btn-primary" href="https://themes.3rdwavemedia.com/" target="_blank">Get in Touch</a>
</div>
마지막으로 헤드의 글자를 바꿉시다.
app/Views/post/layout.php
<head>
<title>Bootstrap 4 Blog Template For Developers</title>
대신
<head>
<title>마크다운 블로그</title>
로 변경합니다.
이제 우리의 마크다운 블로그는 아래와 같이 보입니다.
반응형으로도 잘 보이는지 확인해 봅시다.
브라우저에서 개발자 도구(F12)를 누른 후 반응형 테스트 버튼을 눌러봅니다.
모바일에서는 이렇게 보입니다.
모바일에서 네비게이션을 펼치면 이렇게 보이게 되죠.
아! 혹시 로컬서버에서 하는거라서 버그? 가 보인가봐요~ writable 폴더가 있으면 세션 쓸때 권한설정 해야되고
디비에서 생성하는거면 아무이상 없을거에요.. 로컬에선 잔버그가 좀 보이더라구요