Skip to main content

Vue - Laravel Pagination 연동

· 2 min read

Laravel에서 paginate 메소드를 json으로 받았을 시에 데이터는 다음과 같다.

response
{
"current_page": 1,
"data": [{}, {}, {}],
"from": 1,
"last_page": 1,
"next_page_url": null,
"path": "https://example.com/ajax/list",
"per_page": 15,
"prev_page_url": null,
"to": 8,
"total": 8
}

이 데이터를 blade에서 links 메소드로 쉽게 사용할 수 있는 것 처럼 Vue에서도 그럴 수는 없을까?

Laravel Vue Pagination 패키지를 활용하면 된다. SPA일 경우에는 다운 받고 Usage 탭에 적힌대로 바로 사용하면 되지만, Multi Page일경우는 직접 컴포넌트를 가져와야한다.

사용법

<ul>
<li v-for="post in response.data" v-text="post.title"></li>
</ul>

<nav>
<pagination
:data="response"
:limit="3"
@pagination-change-page="fetchPosts"
/>
</nav>

<script>
new Vue({
data: {
response: {
data: [],
},
},

methods: {
fetchPosts: function (page) {
var vm = this;
axios
.get("url", {
params: {
page: page || 1,
},
})
.then(function (response) {
vm.response = response;
});
},
},
});
</script>

예외처리

Component 소스template 부분을 보면 nav로 감싸져있지 않기에 Laravel 기본 템플릿과 일치시키려면 nav 태그로 감싸주면 된다.

React-Router Code Splitting - 가장 쉬운 방법

· 2 min read

create-react-app으로 생성한 리액트 앱에 시작시 가져오는 컴포넌트 빼고는 비동기로 불러와야 메인이 가벼워진다.

검색해보면 AsyncComponent를 만들라는 게 보이는데, 더 쉬운 방법이 있다.

React-Loadable

React-router 의 Code Splitting탭 에서 찾아볼 수 있는데, 아주 간결하게 컴포넌트를 불러 온다.

import React, { Component } from "react";
import Loadable from "react-loadable";
import Loading from "./Loading";

const LoadableComponent = Loadable({
loader: () => import("./Dashboard"),
/* 컴포넌트 로딩시 보여지는 로딩 컴포넌트 */
loading: Loading,
});

export default class LoadableDashboard extends Component {
render() {
return <LoadableComponent />;
}
}

너무 너무 쉽다. 진작에 이 정도 레벨의 추상화가 있었어야했다.

옵션

자세한 옵션 delay, timeout 등은 여기서 확인할 수 있다.

Laravel 5.5 - Model Collection 데이터 처리하기

· One min read

Model Collection의 데이터를 처리하는 메소드를 사용하고 싶을 때 아래처럼 접근하면 된다.

회원 모델과 사용가능한 포인트를 산출하는 availablePoint 메소드가 있다고 가정한다.

해결

<?php
$members = Member::where('status', 1)
->get()
->map(function($member) {
// Model에서 정의된 filter 메소드를 적용할 수 있다.
return $member->availablePoint();
});

// pagination 에서 사용하는 방법
$members = Member::where('status', 1)->paginate();
// 쉬운 방법
$members->map(function($member) {
return $member->availablePoint();
});

// 긴 방법
//$members->getCollection()->transform(function($member) {
// return $member->availablePoint();
//});

// pagination에서 data 필드만 필요하다면
$members = Member::where('status', 1)
->paginate()
->map(function($member) {
return $member->availablePoint();
});

Vue로 생성된 DOM에 Events를 붙여야할 때

· One min read

data 값이 변경되고 나서 .hover, .click과 같은 jQuery 이벤트를 붙여야할 때, DOM이 다시 그려진 완료 시점을 잡아야한다. Vue에서 nextTick 메소드로 이 시점을 잡을 수 있다. (ajax로 데이터를 가져오지 않고 그려지는 DOM은 mounted 메소드 안에 붙힐 Event 로직을 짜면 된다.)

new Vue({
data: {
members: [],
},

created: function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const vm = this;

axios
.get("/members")
.then(function (response) {
vm.members = response.data;

/* promise가 지원되는 환경에서 */
return vm.$nextTick();

// promise 미지원시
/* vm.$nextTick(function() {
$('.members').hover();
});
*/
})
.then(function () {
$(".members").hover();
});
},
});

인스턴스 메소드를 사용하고 싶지 않다면 Vue.nextTick으로 바꿔주면 된다.

Vue 선언된 data에 chiledren 추가시 렌더링이 안될 때

· 2 min read

data가 이미 정의 되어있고 나중에 데이터를 추가하면 observer가 생성되지 않아 데이터가 갱신이 되어도 DOM이 업데이트가 안 된다.

예시

템플릿

<div id="memberList">
<div v-for="member in members">
{{ member.name }}
<ul v-if="member.logs && member.logs.length > 0">
<li v-for="log in member.logs"></li>
</ul>
</div>
</div>

JS

new Vue({
el: "#memberList",
data: {
members: [{ id: 1, name: "gracefullight" }],
},

mounted: function () {
/* member의 logs 데이터는 그냥 배열로 선언된다. */
this.members[0].logs = [];
/* 데이터를 넣어도 위 템플릿의 <li> 부분이 반복되지 않는다. */
this.members[0].logs = [
{ id: 1, message: "test action", created_at: "2017-11-22" },
];
},
});

해결

set 메소드 또는 $forceUpdate 메소드를 사용하면 된다.

/* 1안 */
const option = {
mounted: function () {
this.$set(this.members[0], "logs", []);
this.members[0].logs = [
// ...
];
},
};
/* 2안 */
const option = {
mounted: function () {
this.members[0].logs = [];
this.members[0].logs = [
// ...
];
this.$forceUpdate();
},
};

adonisjs 시작하기 (nodejs framework)

· 8 min read

개요

nodejs로 정말 간단한 oauth2, jwt 등의 인증을 사용하지 않는 API를 만들기에는 express가 정말 딱이다.

하지만 조금 더 깔끔한 코드를 원하거나 새롭지만 반복적인 기능을 넣기 위해선 더 큰 프레임워크가 필요했다.

조건

언제나 개발시간은 부족하기에 내가 필요한 기능들을 정리해 보았다.

  • await, async를 아무 세팅 없이 사용해 Promise chaining에서 벗어나고 싶다. (BabelWebpack을 직접 세팅하는 시간 걸리는 짓을 안하고 싶다.)
  • 기본적인 웹 보안기능 (validate, csrf, xss, injection) 및 API Throttle Request 기능이 있어야한다.
  • 소셜 로그인passport를 사용하지 않고 한 줄로 끝내고 싶었다.
  • JWT 인증, Log 기능, Bcrypt 암호화를 직접 붙히고 싶지 않다.
  • ORM에 맞게 E-commerce DB를 구조화했기 때문에 ORM이 꼭 필요하다.
  • 직관적이여야 한다. (Model과 Controller가 완벽히 나뉘는 구조를 원했다.)
  • 문서화가 완벽해야한다.
  • 커뮤니티Github Issue가 활성화 되어있어야한다.
  • 관리자 기능을 SSR로 해야할 가능성이 있어 템플릿 기능이 있어야하며, ejs는 절대 쓰고 싶지 않고 다른 템플릿을 직접 세팅해야하는 일은 더더욱 없어야한다.

이 조건을 거르니 adonisjs 라는 처음 보는 프레임워크가 눈앞에 있었다.

성능

image from hexo

나쁘지 않다. 어차피 nginx에서 reverse proxy 처리에, auto scaling 걸꺼고 그래도 더 빠른 속도를 원하면 golang으로 가야지 왜 이 포스트를 보고있나!

  • 여기서 자세한 내용을 확인할 수 있다.

평판

이 프레임워크를 쓰는 다른 사람은 어떻게 생각할까도 궁금했다.

Node.js broken ecosystem and rise of AdonisJs란 포스팅이 마음을 사로잡았다. 원문 읽을 시간이 없다면 이 글의 요약은 이 것이다.

Why not use Express + Sequelize + Config manager + Passport + dotEnv + NodeMailer + Node Validator +30 other modules. How assembling 100+ modules manually can be better than a beautifully pre-configured framework?

한글로 번역하면 왜 안 쓰시죠? 정도가 되시겠다.

시작하기

요구사항

  • Node >= 8.0
  • NPM >= 3.0

1711 기준 8.9가 LTS 버전이기 때문에 그냥 쓰면 된다.

Cli 설치

요즘 멋진 프레임워크들은 다들 cli를 가지고 있다.

yarn global add @adonisjs/cli

앱 설치

adonis new 프로젝트명
cd 프로젝트명
adonis serve --dev

localhost:3333에서 서버가 돌아간다. image from hexo

폴더 구조

.
├── ace
├── package.json
├── public
├── server.js
└── start
├── app.js
├── kernel.js
└── routes.js
파일/폴더설명
aceadonis에서 제공하는 cli 명령어 툴
package.json-
publiccss, js, images와 같은 public 리소스
server.jsHTTP 서버를 부트스트래핑한다. .env 파일의 PORT 변수를 따른다.
start/app.jsadonis 실행시 필요한 기능들을 부트스트래핑한다.
start/kernel.js미들웨어를 등록한다.
start/routes.js라우팅

라우팅

start/routes.js에서 등록한다.

메소드

Route 뒤에 method를 붙히면 된다.

Route.get("/boards", async () => {});

Route.get("/boards/:id", async ({ params }) => {
const board = await Board.find(params.id);
return board;
});

컨트롤러와 연결

Route.get("boards", "BoardController.index");
/* 리소스로 사용시에 */
Route.resource("boards", "BoardController");

/* 리소스에서 create, edit 메소드를 빼고 사용시 */
Route.resource("boards", "BoardController").apiOnly();

/* 구미가 당기는 것만 사용시 */
Route.resource("boards", "BoardController").only(["index", "destroy"]);

/* 구미가 안 당기는 것을 제외할 시 */
Route.resource("boards", "BoardController").except(["index", "destroy"]);

그룹화

보통은 기능이 비슷한 것 끼리 그룹화를 해서 가독성을 높인다.

/* /api/boards routes */
Route.group(() => {
Route.get("/boards", "BoardController.index");
Route.post("/boards", "BoardController.store");
}).prefix("api");

컨트롤러

cli에서 쉽게 생성이 가능하다.

adonis make:controller BoardController

리소스 컨트롤러

리소스가 대단한 건 아니고 미리 정의 된 메소드 7개로 RESTful API를 빠르게 만들기 위한 것이다.

adonis make:controller BoardController --resource

각 메소드들이 연결되는 건 다음과 같다.

  • index: GET boards
  • create: GET boards/create
  • store: POST boards
  • show: GET boards/:id
  • edit: GET boards/:id/edit
  • update: PUT boards/:id
  • destory: DELETE boards/:id

모델

하나의 모델은 하나의 테이블과 매칭된다고 보면 된다.

adonis make:model Board

모델 구조

모델 생성 후에 몇 가지 설정을 해줘야한다.

Board.js
class Board extends Model {
// 테이블 명을 변경해야할 경우
// (테이블 명이 모델명의 복수형이 아닐 경우)
static get table() {
return "board";
}

// 기본 커넥션이 변경될 경우
static get connection() {
return "mysql";
}

// PK 컬럼명이 id가 아닐 경우
static get primaryKey() {
return "uid";
}

// PK의 Auto Increment가 아닐 경우
static get incrementing() {
return false;
}

// 비밀번호 같은 컬럼을 보여주지 않아야 될 경우
// 이 경우 모델에서 fetch 또는 first 메소드로 쿼리빌더를 실행해야된다
static get hidden() {
return ["password"];
}

// 생성일 컬럼이 변경될 경우
static get createdAtColumn() {
return "created_at";
}

// 업데이트일 컬럼이 변경될 경우
static get updatedAtColumn() {
return "updated_at";
}
}

위 속성을 제외하고는 문서를 참조해보자.

기존 코드를 내려받을 때

.env.example 파일을 .env로 복사해 환경설정을 해주고 로그를 남기기 위해 tmp 폴더를 생성해준다

/
$ cp .env.example .env
## .env 파일을 수정하고

$ mkdir tmp

여담

이 프레임워크는 라라벨 스타일로 만들어졌기 때문에 (개념이 같다) 모던 PHP 개발자가 되는 건 식은 죽 먹기가 될 수 있다.

Windows에서 환경변수 cmd로 등록하기

· One min read

매번 내 컴퓨터 > 설정 > 고급 설정 > 환경 변수에 들어가는 걸 그만하고 싶은 사람이라면 다음과 같이 하면 된다.

추가

setx path "%path%;새로운 경로"

refreshenv

설명

환경변수를 등록하고 그 변수를 반영한다.

Linux startup 파일에서 피해야할 것

· 2 min read

스타트업 파일은 사용자가 로그인 할 때 시스템이 어떻게 반응해야 하는지를 결정한다. 스타트업 파일 수정시에 다음 사항들을 꼭 피해야한다.

주의

  • 셸 스타트업 파일에 그래픽 명령을 넣지 않는다.
  • 셸 스타트업 파일에 DISPLAY 환경 변수를 설정하지 않는다.
  • 셸 스타트업 파일에 터미널 유형을 설정하지 않는다.
  • 스타트업 파일에서 표준 출력으로 인쇄하는 명령을 실행하지 않는다.
  • 셸 스타트업 파일에 LD_LIBRARY_PATH를 결코 설정하지 않는다.

LD_LIBRARY_PATH 변수 조작시 런타임 링커가 모든 프로그램에 대해 이 디렉터리들을 찾기 때문에 충돌을 일으킬 수 있고, 라이브러리의 조합이 틀어질 수 있기 때문이다.

추가적으로 디폴트 스타트업 파일에 상세한 주석을 충분히 첨부한다.

Laravel 5.5 - Model Event Listener

· 3 min read

라라벨 이벤트 리스너 기능을 붙혀보자. Model이 Create 될 때 이벤트 리스너를 붙혀 다른 기능을 연결하는 예제가 가장 쉽다. (예를 들면 로그가 생성될 때 SMS를 날리는 경우)

이벤트 생성

EventServiceProvider

먼저 EventServiceProvider에 내가 사용할 이벤트와 리스너를 등록해줘야한다.

app/Providers/EventServiceProvider
<?php
...
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
// 로그 생성시 이벤트를
'App\Events\LogCreated' => [
// 로그 생성됨 리스너에 연결시켜준다.
'App\Listeners\LogCreatedListener',
],
];

$listen 변수에 기본으로 등록되어있는 이벤트는 지워주자

generate

이제 소스 파일을 생성시켜준다.

php artisan event:generate

명령어를 실행하면 app/Eventsapp/Listeners에 방금 등록한 이벤트 리스너 파일이 자동으로 생성된다.

바인딩

모델

모델에서 방금 추가된 이벤트를 연결시켜주자.

app/Models/Log
<?php
use App\Events\LogCreated;

class Log extends Model
{
...
protected $dispatchesEvents = [
// 모델이 create(insert) 되면 해당 이벤트를 호출한다.
'created' => LogCreated::class
// use 구문을 사용하지 않고 여기에 직접 "App\Events\LogCreated" 로 정의해도 될 것 같은데 테스트는 안 해봤다.
];
}

이벤트

이벤트에서 해당 모델을 연결시켜주자.

app/Events/LogCreated
<?php
use App\Models\Log;
...

class LogCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;

// 리스너에서 받을 모델 변수를 public으로 생성한다.
public $log;

// DI
public function __construct(Log $log)
{
$this->log = $log;
}

public function broadcastOn()
{
// 채널을 이용하지 않을 것이기에 빈 배열을 리턴시킨다.
return [];
}
}

처리

리스너에서 받은 이벤트를 처리하자.

app/Listeners/LogCreatedListener
<?php
...
use App\Events\LogCreated;

class LogCreatedListener
{
...
public function handle(LogCreated $event)
{
// Events의 public으로 선언한 데이터가 $event 아래로 바인딩 된다.

$log = $event->log;
logger('LOG Received');
logger($log);

// 여기서 기능을 구현하면 된다.
}
}

ShouldQueue로 확장해 큐에 담을 수도 있다.

여담

메일 발송과 비슷한 플로우였다.

Vue에서 jquery와 bootstrap 전역으로 사용하기

· One min read

expose-loader의 설치가 필요 없는 방법을 사용해보자

Vuejs-kr에 좋은 내용이 있지만 웹팩을 통해 jquery를 꺼내는 방법이 더 간단하다.

설치

yarn add jquery bootstrap

설정

webpack

build/webpack.base.conf.js
const webpack = require("webpack");

module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jquery: "jquery",
"window.jQuery": "jquery",
jQuery: "jquery",
}),
],
};

eslint

.enlintrs.js
module.exports = {
globals: {
$: true,
jQuery: true,
},
};

연동

src/main.js
import "bootstrap";

new Vue({});