Set MIX_ENV in mix task

가끔 테스트 명령이 여럿 필요할 때가 있다. 커버리지 설정을 넣고 싶다던가. 환경변수를 바꿔서 실행해야 한다던다..

Mix에는 이런경우 간단히 추가할 수있는 alias가있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
defmodule Handsup.Mixfile do
use Mix.Project

def project do
[...
aliases: aliases(),
...]
end

defp aliases do
["test.setup": "ecto.create",
"test": ["credo --strict", "ecto.migrate", "test"],
"t": "test"]
end
end

좋다. 이제 실행시켜보면 잘되겠지 싶었는데 dev로 실행시켜 버린다.

1
2
3
4
$ mix t
...
** (Mix) "mix test" is running on environment "dev". If you are running tests
along another task, please set MIX_ENV explicitly

프라이빗 함수를 만들어 알리아스 체인 앞에 넣는다던가 이것 저것 시도해봤지만 뒤의 테스크에 영향을 줄 수 없어서 방치하고 있었는데 전용 옵션이 따로있었다.

1
2
3
4
5
6
7
8
9
10
11
def project do
[...
aliases: aliases(),
preferred_cli_env: preferred_cli_env()
...]
end

defp preferred_cli_env do
["t": :test,
"test.setup": :test]
end

이제 실행해보면 테스트환경에서 잘 실행된다.

Ref

http://elixir-lang.org/docs/stable/mix/Mix.Task.html#preferred_cli_env/1

Phoenix and Webpack

동기

  • 브런치보다 웹팩이 익숙하고 유지보수 잘되고 유저가 많다.
  • 이왕할거면 초기에 하는게 나중에 하는거보다 덜 골치아프다.

환경

사용한 버전은 이렇다.

  • Elixir 1.3.3
  • Phoenix 1.2.1
  • Node 6.6.0

과정

브런치 없이 설치하는게 더 귀찮은데, 이미 오래전에 없이 설치해버려서.. 그냥 둘 다 설명하겠다.

소스 코드확인해도 알 수 있지만 .gitignore의 내용이 다른걸 재외하면, 두 방식의 파일 차이는 이 정도 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-* creating with_brunch/brunch-config.js
-* creating with_brunch/package.json
-* creating with_brunch/web/static/css/app.css
-* creating with_brunch/web/static/css/phoenix.css
-* creating with_brunch/web/static/assets/favicon.ico
-* creating with_brunch/web/static/assets/images/phoenix.png
-* creating with_brunch/web/static/js/app.js
-* creating with_brunch/web/static/js/socket.js
-* creating with_brunch/web/static/assets/robots.txt
+* creating without_brunch/priv/static/css/app.css
+* creating without_brunch/priv/static/favicon.ico
+* creating without_brunch/priv/static/images/phoenix.png
+* creating without_brunch/priv/static/js/app.js
+* creating without_brunch/priv/static/js/phoenix.js
+* creating without_brunch/priv/static/robots.txt

레일스 감각으로 말하면, web/static은 app/assets이고 priv/static은 public/assets 다. frontend같은 다른 경로를 사용해도 되긴하는데 스포클렛처럼 타이트하게 묶여 있거나 묵시적으로 처리하는 부분이 있는게 아니라, 두 경우 모두 web/static을 사용하도록 하겠다.

파일 구조 잡기

브런치 없이 설치한 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ mix phoenix.new without_brunch --no-brunch
* creating without_brunch/config/config.exs
* creating without_brunch/config/dev.exs
* creating without_brunch/config/prod.exs
* creating without_brunch/config/prod.secret.exs
* creating without_brunch/config/test.exs
* creating without_brunch/lib/without_brunch.ex
* creating without_brunch/lib/without_brunch/endpoint.ex
* creating without_brunch/test/views/error_view_test.exs
* creating without_brunch/test/support/conn_case.ex
* creating without_brunch/test/support/channel_case.ex
* creating without_brunch/test/test_helper.exs
* creating without_brunch/web/channels/user_socket.ex
* creating without_brunch/web/router.ex
* creating without_brunch/web/views/error_view.ex
* creating without_brunch/web/web.ex
* creating without_brunch/mix.exs
* creating without_brunch/README.md
* creating without_brunch/web/gettext.ex
* creating without_brunch/priv/gettext/errors.pot
* creating without_brunch/priv/gettext/en/LC_MESSAGES/errors.po
* creating without_brunch/web/views/error_helpers.ex
* creating without_brunch/lib/without_brunch/repo.ex
* creating without_brunch/test/support/model_case.ex
* creating without_brunch/priv/repo/seeds.exs
* creating without_brunch/.gitignore
* creating without_brunch/priv/static/css/app.css
* creating without_brunch/priv/static/js/app.js
* creating without_brunch/priv/static/robots.txt
* creating without_brunch/priv/static/js/phoenix.js
* creating without_brunch/priv/static/images/phoenix.png
* creating without_brunch/priv/static/favicon.ico
* creating without_brunch/test/controllers/page_controller_test.exs
* creating without_brunch/test/views/layout_view_test.exs
* creating without_brunch/test/views/page_view_test.exs
* creating without_brunch/web/controllers/page_controller.ex
* creating without_brunch/web/templates/layout/app.html.eex
* creating without_brunch/web/templates/page/index.html.eex
* creating without_brunch/web/views/layout_view.ex
* creating without_brunch/web/views/page_view.ex

먼저 .gitignore에 node_modules, priv/static/ 을 추가한다. 이 위치는 컴파일 후의 스테틱 파일이 올자리이다.

1
2
3
4
5
6
7
# Static artifacts
/node_modules

# Since we are building assets from web/static,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/

다음은 파일을 옮기고 필요없는 파일을 지운다.

1
2
3
4
5
6
7
mkdir -p web/static/assets/images web/static/js web/static/css
mv priv/static/images/phoenix.png web/static/assets/images/phoenix.png
mv priv/static/robots.txt web/static/assets/robots.txt
mv priv/static/js/app.js web/static/js/app.js
mv priv/static/favicon.ico web/static/assets/favicon.ico
touch web/static/css/app.css
rm -rf priv/static/*

그냥 설치한 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$ mix phoenix.new with_brunch
* creating with_brunch/config/config.exs
* creating with_brunch/config/dev.exs
* creating with_brunch/config/prod.exs
* creating with_brunch/config/prod.secret.exs
* creating with_brunch/config/test.exs
* creating with_brunch/lib/with_brunch.ex
* creating with_brunch/lib/with_brunch/endpoint.ex
* creating with_brunch/test/views/error_view_test.exs
* creating with_brunch/test/support/conn_case.ex
* creating with_brunch/test/support/channel_case.ex
* creating with_brunch/test/test_helper.exs
* creating with_brunch/web/channels/user_socket.ex
* creating with_brunch/web/router.ex
* creating with_brunch/web/views/error_view.ex
* creating with_brunch/web/web.ex
* creating with_brunch/mix.exs
* creating with_brunch/README.md
* creating with_brunch/web/gettext.ex
* creating with_brunch/priv/gettext/errors.pot
* creating with_brunch/priv/gettext/en/LC_MESSAGES/errors.po
* creating with_brunch/web/views/error_helpers.ex
* creating with_brunch/lib/with_brunch/repo.ex
* creating with_brunch/test/support/model_case.ex
* creating with_brunch/priv/repo/seeds.exs
* creating with_brunch/.gitignore
* creating with_brunch/brunch-config.js
* creating with_brunch/package.json
* creating with_brunch/web/static/css/app.css
* creating with_brunch/web/static/css/phoenix.css
* creating with_brunch/web/static/js/app.js
* creating with_brunch/web/static/js/socket.js
* creating with_brunch/web/static/assets/robots.txt
* creating with_brunch/web/static/assets/images/phoenix.png
* creating with_brunch/web/static/assets/favicon.ico
* creating with_brunch/test/controllers/page_controller_test.exs
* creating with_brunch/test/views/layout_view_test.exs
* creating with_brunch/test/views/page_view_test.exs
* creating with_brunch/web/controllers/page_controller.ex
* creating with_brunch/web/templates/layout/app.html.eex
* creating with_brunch/web/templates/page/index.html.eex
* creating with_brunch/web/views/layout_view.ex
* creating with_brunch/web/views/page_view.ex

일단 필요없는 파일을 지운다.

1
2
rm brunch-config.js package.json
rm web/static/css/phoenix.css web/static/js/socket.js

npm, webpack 설정

다음 명령을 실행하고 적당히 내용 입력하면 package.json 파일이 만들어진다.

1
npm init

webpack을 설치하고 설정 파일을 만든다.

1
2
3
npm install --save-dev webpack babel-preset-es2015 copy-webpack-plugin \
babel-loader babel-core \
css-loader extract-text-webpack-plugin style-loader

webpack.config.js파일은 이런 내용이 들어가면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const ExtractTextPlugin = require("extract-text-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin")
module.exports = {
entry: ["./web/static/css/app.css",
"./web/static/js/app.js"],
output: {
path: "./priv/static",
filename: "js/app.js"
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
include: __dirname,
loader: ["babel"],
query: {
presets: ["es2015"]
}
}, {
test: /\.css$/,
loader: ExtractTextPlugin.extract("style", "css")
}]
},
plugins: [
new ExtractTextPlugin("css/app.css"),
new CopyWebpackPlugin([{ from: "./web/static/assets" }])
],
resolve: {
modulesDirectories: [ "node_modules", __dirname + "/web/static/js" ]
}
}

일단 정적파일의 복사, js, css의 생성까지만 다루는 단순한 설정이다.

package.json에 실행 옵션을 포함한 단축명령을 적는다.

1
2
3
4
"scripts": {
"start": "webpack --watch-stdin --progress --color",
"compile": "webpack -p"
},

방금 생성한 명령을 watcher에 추가해 파일 수정이 있을때마다 실행 시킬 수 있다.

config/dev.exs에 넣어두자

1
2
3
config :app_name, AppName.Endpoint,
# 다른 설정은 그대로 둘것
watchers: [npm: ["start"]]

phoenix 자바스크립트들

npm에 등록되어있으니 그냥 설치하면된다.

1
npm install file:deps/phoenix_html file:deps/phoenix --save

위에 패스도 잡아뒀으니, 이전처럼 import해서 사용할 수 있다.

ref

http://matthewlehner.net/using-webpack-with-phoenix-and-elixir/ http://mikker.github.io/2016/02/04/updated-phoenix-webpack-react-setup.html

vi-mode

tl;dr

RTFM

동기

처음에는 이맥스 단축키 외우기 귀찮고 빔이 더 편하지 않을까 정도로 쓰기 시작했다만, 한 2년간 사용했는데도 인터프리터랄께 셸마다 미묘하게 동작이 틀리기도 하고 아예 사용할 수 없는 경우도 있어서 그냥 이맥스 모드도 익숙해져버렸다.

익히고 까먹고 익히고 까먹고를 반복하고 있어서 정리할 겸..

설정

기본적으로는 .inputrc에 추가 하기만 하면 된다.

1
set editing-mode vi

다만, fzf를 사용한다면 오동작이 생기니 .bashrc에서 로드하기 전에 한번 더 설정해줘야 한다.

1
2
set -o vi
[ -f ~/.fzf.bash ] && source ~/.fzf.bash

이것만해도 사용하는데는 크게 문제 없는데, 가끔 지금 어떤 모드에 있는지 몰라서 삽질할 때가 있으니 프롬프트에 현재 상태를 표시하게 해뒀다. .inputrc에 추가로..

1
2
3
set show-mode-in-prompt on
set vi-ins-mode-string \1\e[34;1m\2✚ \1\e[0m\2
set vi-cmd-mode-string \1\e[35;1m\2: \1\e[0m\2

사용하기

전채 목록은 readline의 Default Key Bindings 항목을 읽어보면 된다. 이정도는 알아 둘만 한 것들만 뽑아봤다.

명령 이맥스
맨 앞으로 이동 ctrl-a esc I
맨 뒤로 이동 ctrl-e esc A
단어단위 앞으로 이동 ctrl-b esc b
단어단위 뒤로 이동 ctrl-f esc w
커서위치부터 뒤 삭제 ctrl-k esc D
에디터에서 열기 ctrl-x ctrl-e esc v
히스토리 검색 ctrl-r ctrl-r
주석 처리 alt-# esc I#
클리어 ctrl-l esc ctrl-l

문제점

아까도 말했는데 몇몇 콘솔은 vi mode를 무시한다. 대표적으로 Erlang하고 그 위에서 돌아가는 iex가 그렇다. 물론 래퍼로 감싸서 우회해서 사용할 수는 있지만, 리스크 관리는 알아서;

vi mode는 있는데 명령을 사용할 수 없는 경우도 있다. fish에서는 esc v가 동작하지 않는다.

참고

Elixir tips from exercism

재귀

다른 언어하다와서 성능이 안좋을거라는 선입견에 가능하면 안쓰려고 했는데 괜한 걱정이었다. 오히려 Enum 사용하는 쪽이 느릴 때가 많았다.

for 문

Enum.flat_map/2 대신 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# bad
min..(max - 2)
|> Enum.flat_map(fn x ->
(x + 1)..(max - 1)
|> Enum.flat_map(fn y ->
(y + 1)..max
|> Enum.map(fn z ->
[x, y, z]
end)
end)
end)
|> Enum.filter(&pythagorean?/1)

# good
for x <- min..(max - 2),
y <- (x + 1)..(max - 1),
z <- (y + 1)..max,
pythagorean?([x, y, z]) do
[x, y, z]
end

인자 개수

줄일 수 있으면 줄여라. 짧고 읽기도 편하다.

1
2
3
4
5
6
7
# bad
def upto(0, acc), do: acc
def upto(n, acc \\ []), do: upto(n - 1, [n | acc])

# good
def upto(0), do: []
def upto(n), do: [n | upto(n - 1)]

List

링크드 리스트는 배열과 다르다. 루비에서는 가능하면 배열을 적게 사용하려고 노력했었는데 그럴 필요는 없다. 다만 zip한다던가 할게 아니면 튜플이 나을 때도 있다.

with

갓갓돋는다. BDD감각으로 읽힌다.

& Shorthand

&로 함수 시작해서 인자 넘기는 패턴 밖에 몰랐는데, 인자도 생략할 수 있더라.

1
2
3
4
5
6
~w(1 2 3)
|> Enum.map(&String.to_integer(&1))

# equals to
~w(1 2 3)
|> Enum.map(&String.to_integer/1)

transpose

이게 왜없나 싶은데.. 만들 수는 있다.

1
2
3
4
5
6
7
8
9
def transpose([[]|_]), do: []
def transpose(list) do
[Enum.map(list, &hd/1) | transpose(Enum.map(list, &tl/1))]
end

# or
def transpose(list) do
list |> List.zip |> Enum.map(&Tuple.to_list/1)
end

디스트럭쳐링

패턴 매칭이라 기본적으로 되는데 튜플은 개수 맞춰야하는 거만 좀 조심하면 된다. 맵이 좀 적기 귀찮은데 신텍스 슈가 같은게 있으면 좋겠다. 있는데 나만 모르는 걸 수도 있고..

1
2
3
4
5
6
7
8
9
[a, b, c] = [1, 2, 3]
[a | _] = [1, 2, 3]

{a, b, c} = {1, 2, 3}
{a, _, _} = {1, 2, 3}
{a, _} = {1, 2, 3} # error

%{a: a, b: b} = %{a: 1, b: 2}
%{a: a} = %{a: 1, b: 2}

state

루비에서는 간단하게 돼는데 할일이 많아졌다. 요런 코드가있다고 하자.

1
2
3
4
5
6
7
8
9
class Counter
def initialize
@number = 0
end

def up
@number += 1
end
end

이걸 엘릭서에서 하려면 이렇게 된다.

1
2
3
4
5
6
7
8
9
10
defmodule Counter do
use GenServer

def handle_call(:up, _from, state) do
{:reply, state + 1, state + 1}
end
end

{:ok, pid} = GenServer.start_link(Counter, 0)
GenServer.call(pid, :up)

뭔가 더 단순하게 할 수있는 방법이 있었으면 좋겠다.

그밖에

가능하면 파이프써라.

1
2
3
4
5
# bad
[h] ++ t

# good
[h | t]

까먹을 때가 가끔있는데 List.Chars.t는 Enum.t다. 그냥 홀따옴표 표기로 적어도 Enum돌릴 수 있으니 길게 풀어쓸 필요는 없다.

1
2
3
4
5
6
7
8
9
# bad
?A in [?A, ?B, ?C]
[?A, ?B, ?C]
|> Enum.all?(&(&1 < ?D))

# good
?A in 'ABC'
'ABC'
|> Enum.all?(&(&1 < ?D))

to_integer(float)가 없다. 다들 round(Number) 사용하는 듯 하다.

스트링을 파이프로 넘겨야 할 때는 Regex.replace/4 대신 String.replace/4 쓰면 된다. 인자 순서만 다르고 기본적으로 같은 함수다.

수정 이력

9월 23일: 오타 수정, 링크 추가 9월 28일: 항목 추가