본문으로 건너뛰기

adonisjs 로그 포맷 변경하기 (custom log format)

· 약 2분

아도니스는 로깅 모듈로 winston 을 사용하는데 winston 은 로그를 json 포맷으로 출력하고 심지어 timezone 변수가 따로 나온다.

로그는 기본적으로 프로젝트 폴더 하위의 tmp 폴더에 생성된다 이 폴더가 없으면 로깅이 되지 않으므로 먼저 만들어주자

예를 들면 다음과 같다.

{"level":"info","message":"serving app on http://127.0.0.1:3333","timestamp":"2017-12-21T05:34:50.235Z"}
{"level":"info","message":"serving app on http://127.0.0.1:3333","timestamp":"2017-12-21T05:45:32.220Z"}

이걸 다음과 같이 바꿔보자

[2017-12-21 15:01:03.506] INFO serving app on http://127.0.0.1:3333

오늘 날짜를 구하기 위해 moment를 먼저 설치하자. winston 패키지는 @adonisjs/framework 패키지에 종속된다.

config/app.js
const moment = use("moment");
const { config } = use("winston");

module.exports = {
logger: {
transport: "file",
file: {
driver: "file",
name: "adonis-app",
// 파일명을 바꿔준다
filename: `adonis_${moment().format("YYYYMMDD")}.log`,
level: "debug",
// json 로그 포맷을 해제하고
json: false,
// 원하는 형태로 바꿔준다
formatter: ({ level, message, meta }) => {
const now = moment().format("YYYY-MM-DD HH:mm:ss.SSS");
// 로그 레벨에 색상을 추가하는 작업인데, 굳이 필요하진 않다
const logLevel = config.colorize(level, level.toUpperCase());
const formattedMeta =
meta && Object.keys(meta).length ? "\n\t" + JSON.stringify(meta) : "";

return `[${now}] ${logLevel} ${message || ""} ${formattedMeta}`;
},
},
},
};

버전

  • @adonis/framework: 4.0.28
  • winston: 2.4.0

왜 리덕스 사가(Redux-saga) 인가?

· 약 13분

redux, redux-thunk, redux-promise-middleware, redux-actions, redux-saga 머리가 뽀개질 지경이다. 결국엔 redux-saga를 써야만 했고 왜 saga로 수렴하게 되는지에 대한 삽질기다.

도대체 몇 개의 미들웨어 라이브러리를 파야하는지 화남을 포스팅했기에 오 프론트엔드를 꾸미는데는 리액트가 최고야 라고 생각하는 분들에겐 마음이 안들 수도 있다. 정신건강을 위해 mobx 사용을 추천드립니다.

redux

하위, 상위 컴포넌트에 데이터를 props로 넘겨주는게 너무 관리하기 힘들어서 선택한다. 하나의 Store(Object)에 SPA의 모든 데이터를 보관한다. 대안으로는 Event Bus Component를 만들면 되는데, 하나의 스토어를 갖는게 그게 그거라 거의 강제된다. redux를 적용하기에 앞서 기존 MVC 패턴에 쩌든 사상을 Flex(단방향) 패턴으로 바꾼다는게 거의 남북간 화합 수준이였다. 이 부분에 있어선 연습만이 답이다. 눈감고 코딩할 수 있을 만큼 예제 프로젝트를 반복해보자. 일단 몸에 익으면 action creator가 뭔지 왜 dispatch를 해야하고 reducer로 값을 처리해야 하는지가 뇌에서도 이해가 되는 느낌이였다. (counter 예제는 좋지 않다고 본다. shopping cart예제나 todo 예제가 더 이해하기 쉽다.)

redux-promise-middleware

redux에서 action의 payload를 비동기 데이터로 넘길 때를 알아서 처리해준다. 많은 포스팅이 있는데, 별 쓸모 없다. 실무에서 예제 포스팅을 따라할만한 간단한 비동기 처리 로직은 없다.

redux-thunk

하나의 action에서 여러 개의 다른 action을 호출하던지, action이 dispatch되는 걸 조작할 수 있어, 비동기 처리에도 사용한다. 멋진 라이브러리이지만 클로저 패턴을 사용해야하므로 소스가 지저분하다. (이게 깔끔하다고 생각하면 쓰면 된다. promise then promise then chaning도 깔끔하다고 생각하다면)

redux-actions

action, type, reducer의 반복되는 코드를 더 깔끔하게 구현하길 원한다면 이게 답일 수 있다. 하지만 이 미들웨어는 구문적인 편의성을 제공하는 것일 뿐 비동기를 처리할 수 없어서 promise, thunk 또는 여기서 소개할 saga가 강제된다. 여기를 참조하자.

rematch

1803 최근엔 이 모든 고민을 해결해주는 rematch 라는 라이브러리가 있다 async/await를 사용해 간결한 문법을 제공하는데 아직 삽질해보지 않아 소개정도만 하고 지나가겠다

redux-saga

어떤 action은 promise payload를 뱉고, 어떤 action은 dispatch로 다른 action을 호출하고 어떤 action은 plain object를 뱉고 이런 일관성없는 짓을 계속하다보면 정말 비효율적이라는 느낌을 받는다. 안 받으셨다면 해보시면 느껴질 것이다.

더 극단적으로 든 생각은 Vuex를 사용하면 그냥 mapGetters, mapActions 두 함수만으로 직관적이고 디자이너도 알아볼 법한 코드로 구현이 가능한데 왜 react를 해야되지? 란 의문이 계속 들었다.

Redux-saga를 접하고는 드디어 state 관리의 친구를 만난 느낌이였다. 그렇다면 saga는 뭘까? 한 줄 표현만 기억해보자.

saga란 action에 대한 listener이다. 음.. 액션 리스너구나. 이벤트 리스너같은 것이군.

설치를 하기 전에 reduxgenerator의 개념을 완벽히 이해했다고 가정한다.

설치

# yarn add redux react-redux
$ yarn add redux-saga

create-react-app으로 생성된 프로젝트로 폴더 구조를 다음과 같이 가져갔다.

.
├── public
└── src
├── components
├── containers
└── store
├── actions
├── reducers
├── sagas
└── types
├── package.json
├── App.js
└── index.js

연동

게시글을 가져오는 액션으로 시작해보자. 예시라 모든 코드를 해당 폴더의 index.js에 넣었다. 분리는 편하신대로 하면 되겠다.

빠르게 훑는 generator

  • iterable (돌리고 돌릴 수 있다)
  • 비동기든 동기든 간에 yield 구문으로 순차적 처리가 가능하다.

type

types/index.js
export const FETCH_BOARDS = "FETCH_BOARDS";
export const FETCH_BOARDS_FULFILLED = "FETCH_BOARDS_FULFILLED";
export const FETCH_BOARDS_REJECTED = "FETCH_BOARDS_REJECTED";

action

actions/index.js
export const fetchBoards = () => ({
type: FETCH_BOARDS,
});

// saga에서 호출하는 액션
export const fetchBoardsFulfilled = (boards) => ({
type: FETCH_BOARDS_FULFILLED,
payload: boards,
});

// saga에서 호출하는 액션
export const fetchBoardsRejected = (error) => ({
type: FETCH_BOARDS_REJECTED,
error,
});

action이 pure object만을 반환하는 것을 보고 있으면 아름답다는 생각이 저절로 든다.

reducer

reducers/index.js
const INITIAL_STATE = {
boards: [],
};

export default (state = INITIAL_STATE, { type, payload, error }) => {
switch (type) {
case FETCH_BOARDS_FULFILLED:
return {
...state,
boards: payload,
};
case FETCH_BOARDS_REJECTED:
return {
...state,
showError: true,
error,
};
default:
return state;
}
};

saga

saga는 action을 listen(watch)한다.

saga/index.js
import { call, spawn, put, takeEvery } from "redux-saga/effects";
import * as actions from "../actions";
import axios from "axios";

function* fetchBoardsSaga() {
// try catch finally 구문으로 오류 제어가 가능하다.
try {
// 이부분을 call 메소드를 이용해 테스트가 쉽게 바꿀 수 있다.
// (yeild를 사용하기 때문에 next 명령어로 반복 가능하므로)
// const { data } = yield call([axios, 'get'], '/boards')
const { data } = yield axios.get("/boards");
yield put(actions.fetchBoardsFulfilled(data));
} catch (error) {
yield put(actions.fetchBoardsRejected(error.response));
}
}

function* watchBoard() {
// type의 action이 실행되면 fetchBoardsSaga도 항상(Every) 실행한다
yield takeEvery(FETCH_BOARDS, fetchBoardsSaga);
}

// 모든 listener(watcher)를 하나로 묶어준다.
// rootReducer에 묶어주는 그것과 같다고 보면 된다.
export default function* root() {
yield spawn(watchBoard);
}

왜 watcher들을 spawn으로 묶어야하는지는 이슈에 나와있다. (여기엔 자동으로 saga가 재시작되는 패턴도 있는데, 아직 활용해본 적은 없다)

action => saga => action => reducer 로 연결되는 saga가 완성되었다.

store

스토어 담는건 각자의 취향이니 어떻게 연결하는지만 보면 된다.

store/index.js
import createSagaMiddleware, { END } from "redux-saga";
const saga = createSagaMiddleware();

export default function configureStore(initialState) {
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middlewares),
inDevelopment && window.devToolsExtension
? window.devToolsExtension()
: (f) => f,
),
);

store.runSaga = saga.run;
store.close = () => store.dispatch(END);

return store;
}
index.js
const store = configureStore();
store.runSaga(rootSaga);

메소드

all

yield 구문으로 기다리는 건 순차적으로 실행되기 때문에, 동시에 실행되고 전부 resolve되는 패턴이 필요하다면 all 메소드를 사용하면 된다. (Promise.all을 생각하면 쉽다)

export function* testSaga() {
// 기존 포스팅들에는 이렇게 사용하라는 구문이 많은데
// deprecated 경고를 뱉는다
const [response1, response2] = yield [asyncTask1(), asyncTask2()];

// 아래와 같이 사용하자
const [response3, response4] = yield all([asyncTask1(), asyncTask2()]);
}

call

Function.prototype.call() 함수와 같다.

takeEvery

모든 액션시마다 실행 된다. GET 메소드에 사용하자.

takeLatest

액션 호출시에 같은 액션이 실행 중이면 그 액션은 파기되고 마지막 호출만 실행된다. POST, PUT, DELETE 같은 리소스 변경 메소드에 사용하자.

put

액션을 호출한다. dispatch라고 보면 된다.

다시보기

비유를 통한 설명이 좋으면 이렇게 이해해도 된다. 회사에서 주로 푸드플라이로 서브웨이를 주문하는데 거기에 빗대어 보았다.

1. Board를 가져오는 타입 만들기

  • 서브웨이를 푸드플라이로 시킨다고 한다면
  • const 푸드플라이_서브웨이 = '푸드플라이_서브웨이' 처럼 컴퓨터가 알 수 있게 변수로 행동(액션)의 종류를 설정해주는 것
  • const 푸드플라이_서브웨이_주문성공 = '푸드플라이_서브웨이_주문성공' 처럼 주문이 완료됬을 때의 행동명도 만들 수 있다.

2. fetchBoards 액션 만들기

  • 푸드플라이에서 주문서를 넣는 행동과 같다.
  • 액션은 타입과 결과(payload)를 return하는 함수이다.
  • 굳이 payload를 안 써도 되는데, 그게 예쁘다.

3. fetchBoards 사가 만들기

  • 푸드플라이 서버에서 주문서를 처리하는 것
  • 서버에서 디비 값을 바꾸고 서브웨이로 주문을 밀어넣어주는 것
  • 마지막에 주문이 들어갔다는 다른 액션을 put으로 호출한다.

4. fetchBoards의 액션과 사가를 연결하기

  • 주문서를 넣으면 푸드플라이의 주문서 로직을 실행해야된다는 걸 컴퓨터에게 알려주는 부분
  • takeEvery로 감싸면 매번 주문서가 들어올 때마다 로직이 실행된다는 것
  • takeLatest로 감싸면 매번 주문서가 들어올 때마다 마지막에 들어온 것만 실행하는 것
  • take는 무한루프가 안 예쁘다.

5. fetchBoards 액션이 완료될 경우 발생하는 fetchBoardsFulfilled 액션의 리듀서를 만들기

  • 서브웨이에서 주문서를 받아서 서브웨이 멜트 만드는 준비를 하는 부분

6. 리듀서에서 초기 상태 값을 업데이트 해주기

  • 초기상태값은 서브웨이 멜트에 터키가 최소 2장, 햄 최소 2장 들어간다 같은 것
  • 업데이트는 서브웨이 멜트를 진짜 만드는 행동

7. 컴포넌트(클래스)를 리덕스 스토어와 연결

8. Boards Prop과 fetchBoards Action을 가져오기 (map...toProps 아실 거에요)

9. componentDidMount, onClick에서 fetchBoards 실행

10. Boards.map 등 DOM에 바인딩

여담

  • Saga API Docs엔 action을 기다리는 take 기능 등 많은 고급 기능이 있다.
  • supervisor 패턴으로 구성한 사람도 보이는데 아직 필요성을 잘 못 느끼겠다.
  • 처음부터 mobx로 시작했어도 쉬웠을텐데
  • saga에서 쓰는 개념들은 rx의 스트림과 비슷하게 느껴졌다.
  • 2020년에는 Redux hooks를 사용하면 된다.

잘 이해가 안 된다면 mobx를 이해하는 게 정신건강에 좋습니다.

Vue - Laravel Pagination 연동

· 약 2분

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분

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

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

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 데이터 처리하기

· 약 1분

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를 붙여야할 때

· 약 1분

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분

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분

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로 등록하기

· 약 1분

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

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

$ refreshenv

설명

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

Linux startup 파일에서 피해야할 것

· 약 2분

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

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

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

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