라라벨 인증 시험 통과했어요

지난 주말에 시험을 쳤는데 결과가 빨리 나왔네요.

아리송한 문제들이 많아서 재수하겠다 싶었는데, 한 번에 통과해서 다행이에요. ㅎㅎ

+ 후기를 보고 싶단 코멘트가 있어서 추가로 남겨봅니다.

결제

결제는 작년 11월 말에 했어요. 2020년 내에 응시하자는 마음으로 미리 결제했어요. 결제하고 바로 보는건 아니고 3달 이내에 응시하면 되거든요. 돈을 내 놓으면 돈 아까워서라도 봐야하니까 ㅎㅎ 비용은 163.9 유로였습니다.

결제하고나면 로그인 할 수 있는 계정을 안내받는데, 요 메일이 바로바로 오지 않아서 조금 답답했습니다.

결제를 했는데 메일이 안오다니! 바쁘다바빠 현대인에게 있을 수 없는 일!! 못 참고 막 이메일 보내벌임. ㅎㅎ

몇시간 지났나 하루가 지났나 기억이 잘 안나는데 암튼 메일이 오긴 왔습니다. 독촉해서 보내준건 아닌거 같고 걍 느긋하게 처리하는거 같아요.

준비

당시에 처음부터 제대로 배우는 라라벨 원고를 퇴고하고 있었기 때문에 따로 준비는 하지 않았습니다. 어차피 공부하는거 자격증까지 딸 수 있으면 좋겠단 생각이었어요. 떨어지면 쪽팔리니까 몰래보고 떨어지면 없었던 일로 하려고 했죠. ㅎㅎㅎ

원래 계획과 달리 번역서 출시 후 바로 시험을 치루진 못했어요. 어영부영 두 달이 지나버려서 시험 치를 기회를 노리고 있었는데, 지난 주말에 아이들이 처가에 가서 자고 오는 덕에 시험을 치렀습니다. 책 읽은 기억도 흐려지고 준비도 따로 안해서 자신은 없었는데 떨어지면 나만 알면 되니까 ㅋ

시험

시험에 대해서는 자세한 내용은 말하지 못하게 되어있어요.(아마? 시험 전에 안내가 나왔는데 대충 읽어서 ㅎㅎ) 그러니 간단히 소감 정도만 쓰겠습니다.

시험은 저한테는 좀 어려웠어요. 알쏭달쏭한 문제가 많았습니다. 평소에 잘 알던 부분도 ‘그래서 정확히 이거야? 저거야?’ 요런식으로 물으니 쫄리더라고요 ㅋ 게다가 틀리면 더 감점이 되는 구조라, 모르면 차라리 답 안하고 넘어가는게 낫거든요. 참 모든 문제는 4지선다형 입니다.

문제는 45문제 시험 시간은 1시간 입니다.(50분이었나? 가물가물) 문제 내는 알고리즘은 좀 별론지 같은 문제가 중복으로 나오기도 했습니다. 같은 문제가 지문만 살짝 다르게 나온 경우도 있어서 주의해야 합니다.

시험 볼 때는 부정 행위를 방지하기 위해서 제 모습과 화면을 녹화해갑니다. 밝은 방에 혼자 있어야하고, 시험보는 브라우저 외에 다른 브라우저나 탭을 띄우면 안되요. 책이나 기타 자료를 참고하는 것도 안됩니다.

결과

결과 통보는 일주일 정도 걸린다고 본 것 같은데, 의외로 하루 만에 오더군요.

결과는 붙었다고만 나오고 몇개 맞았는지, 뭘 틀렸고 답이 뭔지는 안 알려줍니다. 혹시 다 통과 시켜주는 건 아닌가하는 의심이…

별로 쓸 얘기 없을 거 같았는데 막상 쓰다보니 길어졌네요. 🙂

라라벨 라우트 그룹 사용시 유의사항

라라벨 코리아 페이스북 그룹에 질문이 하나 올라왔습니다.

https://www.facebook.com/groups/laravelkorea/permalink/1862353923927678

처음부터 제대로 배우는 라라벨의 예제 코드 처럼 라우트 그룹을 작성하면 에러가 나는데, 책에 나온 코드 처럼 사용하는 방법은 없느냐 하는 것 입니다.

질문해주신 분이 언급한 예제는 79 페이지의 예제 3-10 이며 내용은 아래와 같습니다.

Route::group(function () {
    Route::get('hello', function () {
        return 'Hello';
    });
    Route::get('world', function () {
        return 'World';
    });
});

위의 코드를 실행하면 질문자분께서 말씀하신 것 처럼 에러가 납니다.

“Illuminate\Routing\Router::group(): Argument #1 ($attributes) must be of type array, Closure given, called in …”

group() 메서드의 첫번째 인자로 배열이 들어와야하는데 클로저가 들어왔다는 내용입니다.

결론부터 말씀드리면 책의 예제 코드가 잘못된 게 맞습니다. ㅠ

Illuminate\Routing\Router::group() 메서드의 시그너처가 아래와 같기 때문에 첫번째 인자로 반드시 배열을 넘겨줘야 합니다.

public function group(array $attributes, $routes)

따라서 예제 코드는 아래와 같이 바뀌어야 정상 동작합니다.

Route::group([], function () {
    Route::get('hello', function () {
        return 'Hello';
    });
    Route::get('world', function () {
        return 'World';
    });
});

클로저만 넘기는 경우도 있던데요?

Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/photos', function() {
        //
    });
});

위 코드는 81 페이지의 예제 3-12 입니다.

group() 메서드에 클로저 하나만 넘겨주고 있습니다. 어떻게 된거죠?

group(), 동명이인

이유는 Route::group() 과 Route::middleware()->group() 에 쓰인 group()이 서로 다른 클래스의 메서드이기 때문입니다.

Route::middleware()는 Illuminate\Routing\Router가 아닌 Illuminate\Routing\RouteRegistrar를 반환합니다. 따라서 middleware() 메서드에 체이닝된 group() 메서드는 Illuminate\Routing\Router의 group() 메서드가 아닌 Illuminate\Routing\RouteRegistrar의 group() 메서드인 것이죠.

public function group($callback)

Illuminate\Routing\RouteRegistrar의 group() 메서드 시그너처가 위와 같기 때문에 클로저만 넘겨주면 됩니다.

권장사항

위의 내용을 종합해보면 라우트 그룹을 사용하는 방법이 두 가지인 셈입니다.

  1. Route 퍼사드에서 group() 메서드를 바로 호출하는 방법
  2. Route 퍼사드에서 middleware(), domain(), prefix(), name() 등의 메서드에 체이닝하여 group() 메서드를 호출하는 방법

저는 2번 방식을 권장합니다. 첫째, 1번은 메뉴얼에 안내되지 않는 사용법입니다. 둘째, 그룹을 묶는다는 것은 언제나 목적이 있을 것입니다. 따라서 위에 수정된 예제 3-10의 코드 처럼 빈 배열을 이용해서 그룹을 만드는 건 의미가 없습니다. 그룹을 묶는 목적에 따른 메서드를 모두 제공하고 있으니, 각 메서드를 활용해서 그룹을 만드는게 보기에도, 관리하기에도 더 나을 겁니다.

[1일 1식 라라벨] 모델 변경 이력을 자동으로 저장해주는 패키지 Revisionable

위키백과의 “라라벨” 문서가 수정된 내역

위키의 핵심 기능은 과거의 모든 변경 내역을 조회할 수 있고, 원하면 과거 버전으로 쉽고 되돌아갈 수 있는 것이라 생각한다.
간혹 위키 같이 과거의 변경 내역을 기록으로 남기고 조회하는 기능이 필요할 때가 있다. 내가 운영하는 카페에서는 사물함 관리에 이 기능이 필요했다. 사물함 대여자 정보를 업데이트 할 때 실수를 할 수 있으므로, 변경되기 전의 데이터가 어딘가에 남아있어야 했다. 당시에는 단순하게 똑같은 테이블을 하나 더 만들어서(lockers 테이블과 똑같은 lockers_logs 테이블을 만드는 식으로) 업데이트 전에 백업하는 식으로 구현했다.

이런 기능이 내게만 필요했던 건 아니었는지 크리스 듀엘(Chris Duell)이 Revisionable이라는 패키지를 만들었다. 이 패키지를 사용하면 모델에 변화가 있을때마다 자동으로 ‘누가’, ‘무엇을’, ‘언제’, ‘어떻게’ 수정했는지가 저장된다. 수정내역을 남기고 싶은 모델에 RevisionableTrait 트레이트를 사용한다고 선언하기만 하면 된다. 사물함 관리 기능 만들 때 이 패키지가 알았더라면 ㅠ

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Venturecraft\Revisionable\RevisionableTrait;

class Post extends Model
{
    use RevisionableTrait;
}

실전에서 어떻게 쓰이는지 보는게 가장 와 닿을 것 같다. 내 카페용 애플리케이션에 관심있는 사람들은 없겠지만 겸사겸사 한 번 적용해보겠다.

설치

컴포저로 설치한다.

composer require venturecraft/revisionable

설치가 완료되면 config/app.php 파일의 providers 항목에 RevisionableServiceProvider를 등록한다.

'providers' => [
    Venturecraft\Revisionable\RevisionableServiceProvider::class,
]

설정 파일과 마이그레이션 파일을 퍼블리싱한다.

php artisan vendor:publish --provider="Venturecraft\Revisionable\RevisionableServiceProvider"

// 제대로 진행된다면 아래와 같은 결과 메시지가 터미널에 출력된다.
Copied File [/vendor/venturecraft/revisionable/src/config/revisionable.php] To [/config/revisionable.php]
Copied Directory [/vendor/venturecraft/revisionable/src/migrations] To [/database/migrations]
Publishing complete.

Revisionable 패키지는 모든 모델의 수정내역을 하나의 테이블로 관리한다. 마이그레이션을 실행한다.

php artisan migrate

데이터가 어떻게 저장되는지 확인하기 위해 마이그레이션 파일을 살펴보자.

public function up()
{
    Schema::create('revisions', function ($table) {
        $table->increments('id');
        $table->string('revisionable_type');
        $table->integer('revisionable_id');
        $table->integer('user_id')->nullable();
        $table->string('key');
        $table->text('old_value')->nullable();
        $table->text('new_value')->nullable();
        $table->timestamps();

        $table->index(array('revisionable_id', 'revisionable_type'));
    });
}

revisionable_type은 수정한 모델의 타입이고 revisionable_id는 수정한 모델의 ID 값이다. 이런 방식으로 하나의 테이블에서 모든 모델의 변경사항을 다루는 걸 일대다 다형성 관계라고 한다. 다형성 관계에 익숙하지 않은 사람들은 매뉴얼을 참고하자.

key는 모델의 어떤 값이 변경되었는지를 저장하는 값이다. 예를 들어 Post 모델의 title이 수정되었다면 key에 ‘title’이 값으로 저장된다.

카페용 애플리케이션에서 사물함은 Locker 모델로 관리했다. Locker 모델에 RevisionableTrait를 끼워넣는다.

<?php

namespace App;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Venturecraft\Revisionable\RevisionableTrait;

class Locker extends Model
{
    use RevisionableTrait;

    ...이하 생략

Locker를 업데이트하는 LockerService::update()는 원래 아래와 같았다.

class LockerService
{
    public function update($request, $locker)
    {
        DB::transaction(function () use ($request, $locker) {
            $this->backup($locker);

            $inputs = $this->buildInputsForUpdate($request, $locker);

            $locker->update($inputs);
        });
    }

코드는 간단하다. 백업하고 업데이트할 데이터를 준비해서 업데이트한다.

이제 Revisionable이 수정내역을 관리해주기 때문에 백업하는 코드는 더이상 필요 없다. 그래서 $this->backup($locker); 줄을 지우고 LockerService::backup() 메소드를 통째로 제거했다.

테스트로 데이터를 하나 변경해봤다. 홍길동이 대여하고 있던 사물함을 장길산이 사용하게 된 시나리오다.

‘사용자’, ‘이용 시작 시점’, ‘이용 종료 시점’, ‘비밀번호’가 바뀌었다. revisions 테이블을 보면 아래와 같이 4개의 데이터가 추가되었다. 

변경 행위는 한 번인데, 변경된 필드마다 데이터가 하나씩 생성되는 점은 조금 아쉽다. 현재로서는 같은 행동이 원인이 되어 바뀌었는지 여부를 판단할 수 있는 유일한 방법은 revisions 테이블의 created_at 이 같은지 확인하는 방법 뿐이다. 수정내역을 사용자의 행동 단위로 묶을 수 있도록 식별값을 하나 더 추가했으면 어땟을까 싶다.

기본적인 기능 외에 아래와 같이 다양한 옵션을 제공한다. 필요할 때 패키지의 매뉴얼을 보고 활용하자.

  • 변경 뿐만 아니라 생성 기록도 저장하기
  • 저장할 필드, 혹은 제외할 필드 지정하기
  • 수정내역 저장 비활성화
  • 수정내역 최대 저장 갯수 지정
  • 출력 형식 변경하기

이 글은 7월 2일자 1일 1식 라라벨에 발행된 글입니다. 8월호 구독자를 모집하고 있습니다. 월 1만원으로 최신 라라벨 소식을 받아보세요.

라라벨에서 테이블 변경 마이그레이션 추가 후 테스트가 깨졌다

개발 단계에서는 테이블 생성하는 마이그레이션을 고쳐가며 작업해도 괜찮습니다. 하지만 이미 애플리케이션을 배포한 뒤라면 그럴 수 없죠. 기존 테이블을 수정하는 마이그레이션을 작성해야 합니다.

오늘은 기존 테이블을 변하는 마이그레이션을 작성하다가 겪은 일을 공유하고자 합니다.

아래와 같이 아주 간단한 마이그레이션을 작성했습니다.

public function up()
{
    Schema::table('posts', function(Blueprint $table) {
       $table->renameColumn('movie_id', 'postable_id');
       $table->string('postable_type')->default('');
    });
}

posts 테이블에서 movie_id 컬럼명을 postable_id로 바꾸고, postable_type 컬럼을 하나 추가하는 것입니다.

개발 환경에서는 마이그레이션이 문제 없이 실행되었는데, 테스트를 돌려보니 에러가 납니다. 에러 메시지를 보니 postable_type 컬럼이 없다고 합니다.

table posts has no column named postable_type

분명히 개발 환경에서 마이그레이션할 때 문제가 없었는데 어떻게 된 일일까요?

문제는 SQLite 때문이었습니다. 저는 테스트 속도를 높이기 위해 테스트시에는 SQLite를 쓰고 있습니다. 근데, SQLite는 테이블을 수정할 때 한 번에 여러 컬럼을 추가할 수가 없다고 하네요. 그래서 테스트 환경에서는 두번째 작업인 postable_type 컬럼 추가가 진행이 안된 것입니다.

해결 방법은 간단합니다. 수정하는 마이그레이션인 경우 한 번에 하나씩만 바꾸도록 코드를 바꿔주면 됩니다.

 public function up()
{
    Schema::table('posts', function(Blueprint $table) {
       $table->renameColumn('movie_id', 'postable_id');
    });

    Schema::table('posts', function(Blueprint $table) {
        $table->string('postable_type')->default('');
    });
}

물론 테스트 할 때 SQLite 안쓰시는 분은 신경쓰지 않으셔도 됩니다.

라라벨 5.8.16 새기능(2)

라라벨 5.8.16에서는 이전에 소개한 마이그레이션 이벤트 이외에 두가지 기능이 더 추가 되었습니다.

하나는 PostgreSQL을 사용하는 사람을 위한 기능으로, migrate:fresh 할 때 type을 지울 수 있는 옵션이 추가된 것입니다. 개발자에 의하면 PostgreSQL 에서는 ENUM에 타입을 사용하는데 migrate:fresh를 하면 테이블은 다 지워지지만 이 타입이 남아서 문제가 생겼었다고 하네요. 데이터베이스 뷰를 지우는 옵션을 사용하는 것과 같은 방법으로 사용하면 된다고 합니다. (데이터베이스 뷰를 지우는 옵션도 있었군요…ㅎㅎ)

php artisan migrate:fresh --drop-types

다른 하나는 MailMessage 클래스에 Renderable 컨트랙트를 추가한 것입니다. 이를 통해 알림(Notification) 메일이 어떻게 보내질 것인지 브라우저로 확인해볼 수 있다고 하네요. 예를 들어, 컨트롤러에서 다음과 같이 하면 된다고 합니다.

return (new FooNotification())->toMail('example@example.com');

브라우저로 메일 내용을 미리보는 건 Mailable 클래스에 이미 있던 기능인데, 같은 기능을 알림 메일에 사용하는 MailMessage 클래스에도 추가했다고 합니다.

그렇지 않아도 이번에 막 알림을 메일로도 받을 수 있게 작업하려는 참이었는데 잘됐네요 ㅎㅎ

라라벨 5.8.16 새기능(1) – 마이그레이션 이벤트 추가

Illuminate\Database\Events 네임스페이스에 아래 이벤트가 추가되었습니다.

  • MigrationEnded
  • MigrationsEnded
  • MigrationStarted
  • MigrationsStarted

이 기능을 제안하고 추가한 알렉스 보워스가 밝힌 용도는 마이그레이션을 시작할 때 캐시를 지우거나, 마이그레이션 시작과 종료를 모니터링하는 것 등 입니다.

참고

라라벨 5.8.9 – 이벤트 발견 기능 추가

5.8.8 까지는 어떤 이벤트가 발생하면 어떤 리스너가 작동해야하는지 직접 적어줬어야 했습니다. 아래와 같은 식이죠.

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'App\Events\OrderShipped' => [
        'App\Listeners\SendShipmentNotification',
    ],
];

5.8.9 부터 이벤트 발견 기능이 추가되어 이벤트와 리스너의 관계를 직접 등록하는 수고를 덜게 됐습니다.

이벤트 발견 기능은 기본적으로 비활성화 되어 있습니다. 이벤트 발견 기능을 사용하려면 아래와 같이 EventServiceProvidershouldDiscoverEvents 메소드를 오버라이드해야 합니다.

/**
 * Determine if events and listeners should be automatically discovered.
 *
 * @return bool
 */
public function shouldDiscoverEvents()
{
    return true;
}

이벤트 발견 기능을 활성화하면 이벤트 캐시 파일이 있으면 이를 이용하고, 없으면 실시간으로 이벤트를 찾습니다. 실시간으로 리퀘스트를 찾으면 애플리케이션이 느려지기 때문에 되도록 캐시를 사용하는게 좋습니다. artisan event:cache 명령으로 캐시할 수 있습니다.

Laravel Collection 메소드 중 concat과 push의 차이

라라벨 5.8.8에서 쿼리 빌더에 forPageBeforeId 메소드가 추가되었습니다. 뭔지 알아보려고 PR을 보는데, 예제에서 컬렉션 메소드 중 concatpush를 쓰더군요.

$posts = new Collection;

$posts = $posts->concat(ChatPost::forPageBeforeId(15, $focused->id)->get()->reverse());

$posts = $posts->push($focused);

$posts = $posts->concat(ChatPost::forPageAfterId(15, $focused->id)->get());

둘 다 제가 잘 안쓰던 메소드들이라 메뉴얼을 찾아봤는데, 읭? 둘이 똑같아 보이는 겁니다.

push 메소드는 컬렉션의 마지막에 아이템을 추가합니다:

concat 메소드는 주어진 배열 또는 컬렉션의 마지막에 값을 추가합니다:

그래서 소스를 봤는데, 소스를 보니 차이점을 알겠더라구요.

    /**
     * Push all of the given items onto the collection. 
     *
     * @param  iterable  $source
     * @return static
     */
    public function concat($source)
    {
        $result = new static($this);

        foreach ($source as $item) {
            $result->push($item);   // 주어진 아이템들을 push를 사용해서 추가
        }

        return $result;
    }

push는 하나의 아이템을 컬렉션의 마지막에 추가하는 거고, concatpush 메소드를 이용해서 여러 아이템을 한 번에 추가하는 거였어요.

뭐 별로 중요한 얘기는 아니었습니다 하핫

익혀야할 것

오늘 업무를 종료하며 내일은 아래 두 가지를 익혀야겠다고 생각했습니다.

  1. Laravel HTTP 테스트에서 Mockery를 사용하는 방법
  2. Laravel HTTP 테스트 실행시 xdebug 로 디버깅하는 방법

오늘은 테스트를 작성하면서 삽질을 많이했는데, 첫번째 것은 오늘 삽질 결과 알아낸 해결책이고, 두번째 것은 오늘과 같은 삽질을 덜 고통스럽게 하는 해결책입니다.

Laravel elixir version 기능이 제대로 작동하지 않는 경우

몇 주만에 라라벨로 만든 애플리케이션을 수정하려고 했는데, gulp 명령어를 실행하니 에러가 났습니다.

SyntaxError in plugin 'run-sequence(version)'
Message:
Unexpected token s in JSON at position 41
Stack:
SyntaxError: Unexpected token s in JSON at position 41
at Object.parse (native)
at VersionTask.deleteManifestFiles (/home/vagrant/Code/bookcafe100.com/node_modules/laravel-elixir/dist/tasks/VersionTask.js:113:29)
at VersionTask.gulpTask (/home/vagrant/Code/bookcafe100.com/node_modules/laravel-elixir/dist/tasks/VersionTask.js:71:18)
at VersionTask.run (/home/vagrant/Code/bookcafe100.com/node_modules/laravel-elixir/dist/tasks/Task.js:138:31)
at Gulp.<anonymous> (/home/vagrant/Code/bookcafe100.com/node_modules/laravel-elixir/dist/tasks/GulpBuilder.js:65:67)
at module.exports (/home/vagrant/Code/bookcafe100.com/node_modules/orchestrator/lib/runTask.js:34:7)
at Gulp.Orchestrator._runTask (/home/vagrant/Code/bookcafe100.com/node_modules/orchestrator/index.js:273:3)
at Gulp.Orchestrator._runStep (/home/vagrant/Code/bookcafe100.com/node_modules/orchestrator/index.js:214:10)
at Gulp.Orchestrator.start (/home/vagrant/Code/bookcafe100.com/node_modules/orchestrator/index.js:134:8)
at runNextSet (/home/vagrant/Code/bookcafe100.com/node_modules/run-sequence/index.js:86:16)

한참 삽질하다가 Elixir version is messing up 라는 글에서 ‘public/build/rev-manifest.json 파일이 아래와 같이 되어 있기 때문이라는 걸 알았습니다.

{
"js/app.js": "js/app-e81f312e80.js"
}s"
}

아직까지 rev-manifest.json 파일이 왜 저리 되었는지 원인은 못찾아냈습니다만, 문제를 유발하는 s” } 를 지우니 빌드가 됐습니다. 아오 답답해서 혼났네요.

 

leaderboard-728x90