[Phoenix Framework] Programming Phoenix 1.4 - Ch.3. Controller

posted by donghyun

20 min read

태그

사내 스터디에서 본인이 번역한 내용 일부

Chapter 3. 컨트롤러

전통적인 request는 한 endpoint에서 시작해서, 한 router로 들어가, controller로 흐릅니다. 먼저 웹 프로그래밍이 하나의 함수형 문제이고, 함수형 언어를 사용해 표현하는 것이 자연스럽다는것을 이해하는것부터 시작해야합니다.

이제 Hello World 앱 예제는 버리고, 이 책에서는 앞으로 새로운 프로젝트를 만들어 끝까지 그것을 발전시켜가며 진행해 볼 것이다. 일단 시작하기 전에 컨트롤러의 작동방식에 대해 자세히 살펴볼 것입니다. 또한 뷰, 템플릿 및 컨텍스트를 포함하여 컨트롤러가 다루는 모든 사항을 소개합니다.

Controller 이해하기

이 챕터에서 컨트롤러들과 컨트롤러가 다루는 앱의 부분들을 작성하는 것에 집중할 것입니다. 피닉스는 쉽게 간단한 웹앱을 scratch로부터 생성하는 generator가 있지만, 우리는 직접 작성해서 부분들이 어떻게 결합되는지를 이해해볼 것입니다..

rumbl이라는 앱을 만들 겁니다. 완성하고 나면, 앱을 통해 다른곳에 호스팅된 비디오를 가져오고 실시간으로 거기에 댓글을 달고, 다른 사용자의 댓글과 함께 비디오를 재생할수 있을 것입니다. 규모에 따라 이 앱은 요구사항이 엄청 커질 것인데, 각 사용자가 기록하고 재생하는 댓글들은 빠르게 저장되고 적절하게 제공되어야 하기 때문이죠. 대충 다음 그림과 같습니다.

screenshow-phoenix-1 기본 개념부터 익혀보도록 동영상과 댓글 처리 전에 사용자부터 다뤄봅시다. 처음에는 사용자를 처리하는 컨트롤러에 중점을 둘 것입니다. 먼저 사용자 컨트롤러에 대한 요청이 브라우저를 통해 들어오면 어떤 동작이 이뤄지길 바라는지 적어봅시다.

connection
|> endpoint()
|> router()
|> browser_pipeline()
|> UserController.action()

요청 하나는 엔드포인트로부터 들어오고(lib/rumbl/endpoint.ex) router로 들어갑니다.(web/router.ex). router는 URL 패턴을 매치시키고 브라우저 파이프라인을 통해 연결을 전달한 다음 UserController를 호출합니다. 요청이 index action을 호출한다고 가정하고 마지막 부분을 좀더 세분화해보겠습니다.

connection
|> UserController.index
|> UserView.render("index.html")

이제 각 요청에 작동하는 컨트롤러를 만들고, view가 template을 render하도록 하고, 또한 template을 만들어야 합니다. route도 연결해야합니다.

브라우저 요청에 응답하려면 비즈니스 로직을 구현해야 합니다. 예를 들어 UserController.index는 앱의 모든 사용자를 반환하는 간단한 함수일 수 있지만, 그 사용자들은 어디 저장되어 있을까요? index view가 각 user마다 어떤 정보가 필요한가요? 사용자들에게 다른 역할이 있을까요? 모든 사용자 정보가 공개되어있나요? 이것들이 앱마다 다른 비즈니스 관심사입니다.

이 비즈니스 로직을 모두 컨트롤러에 직접 쓸 수도 있지만 더 좋은 방법이 있습니다. 비즈니스 로직을 context라고 하는 간단한 API 계층으로 분리시키겠습니다.

Context

Phoenix의 context는 function을 공유 목적으로 그룹화하는 모듈에 지나지 않습니다. 예를 들어, 앱은 사용자 계정을 읽고 수정하고 삭제해야 합니다. 이 코드를 모두 단일 모듈로 유지하려고 노력할 것입니다.

일반적으로 컨텍스트는 모든 비즈니스 로직을 공통의 목적으로 캡슐화합니다. 이러한 방식으로 코드를 중복시키지 않고도 컨트롤러, 채널 또는 원격 API에서 비즈니스 로직과 상호작용이 가능합니다. 좀 간단히 표현해보면 컨트롤러는 컨텍스트 함수와 함께 동작하기 위해 존재한다고 할 수 있습니다. 컨트롤러는 최종 사용자 요청을 파싱하고 컨텍스트 함수를 호출하며 이러한 결과를 최종 사용자가 이해할 수 있는 것으로 변환합니다. 각 코드 조각은 분리된 목적을 가지고 있습니다. 컨텍스트는 컨트롤러에 대해 알지 못하며 컨트롤러는 비즈니스 규칙에 대해 알지 못합니다.

앞으로 살펴 보겠지만, 컨텍스트에서 코드를 구성하는 것도 유지관리에 뛰어납니다. 웹 레이어를 전혀 건드리지 않고 컨텍스트를 변경하여 새로운 기능, 수정 사항 또는 비즈니스 로직 변경 사항을 추가할 수 있습니다. 또한 컨텍스트 API를 통해 비즈니스 로직을 꼼꼼하게 unit 테스트하면서 한편으로는 컨트롤러에 대한 통합 테스트에 집중할 수 있습니다. 이제 프로젝트 구축을 시작해보죠.

요약: 컨텍스트는 비즈니스 로직 캡슐화, 컨트롤러는 비즈니스 규칙 모른채로 공개된 인터페이스 쓰듯이 컨텍스트 활용. 통합테스트와 비즈니스 로직 유닛 테스트 분리 가능.

Creating the Project

mix phx.new 명령어로 rumbl이라는 새 앱을 만듭니다. 나중에 골치아파지는걸 피하기 위해, Ecto 설정을 만들고 일단 잘 돌아가게 해 놓을 것입니다.

mix phx.new rumbl
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix deps.compile
* running cd assets && npm install && \
 node node_modules/webpack/bin/webpack.js --mode development We are all set! Go into your application by running:
 $ cd rumbl
Then configure your database in config/dev.exs and run:
 $ mix ecto.create
Start your Phoenix app with:
 $ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
 $ iex -S mix phx.server

먼저, mix ecto.create 을 실행해서 나중을 위해 데이터베이스를 준비합니다. 그 다음, mix phx.server 명령어로 app을 실행하고 작동하는지 봅니다. 브라우저가 http://localhost:4000/ 을 가리키도록 해보죠. 친숙한 Phoenix home page를 보게 될 것입니다. 원하던건 아니지만 일부 좋은 부분은 가져다 직접 메시지 만드는데 쓸 것입니다.

간단한 홈페이지

기본 제공되는 웹페이지는 간단한 HTML 템플릿입니다. 이것을 당장 우리 홈페이지의 기초를 만드는데 활용할 수 있습니다. index파일을 다음과 같이 조정해봅시다.

# rumbl/lib/rumbl_web/templates/page/index.html.eex
<section class="phx-hero">
  <h1><%= gettext "Welcome to %{name}!", name: "Rumbl.io" %></h1>
  <p>Rumbl out loud.</p>
</section>

원하던 홈페이지를 얻었습니다. 이제 사용자를 위해 무엇을 해야할지 생각해봅시다. 실제 데이터베이스를 통합할 준비가 되진 않았지만 컨트롤러가 데이터베이스를 사용할 수 있는 방법에 대해 생각할 수 있습니다. 이를 위해 컨텍스트를 작성합니다.

Working with Context

훌륭한 프로그래머라면 복잡한 아이디어를 개별 step으로 나누는 방법을 배워야 합니다. 반대도 마찬가지고요.

아름다운 API를 구축하려면 전략적으로 function들을 계층화하고 그룹화하여 개별 function을 아이디어로 통합할 수 있어햐 합니다.

우리가 사용하는 모든 라이브러리, 심지어 Elixir 자체도 이러한 아이디어를 기반으로 구성됩니다. 예를 들어 Elixir의 표준 라이브러리에서 Logger.debug를 호출 할 때마다 Logger 컨텍스트에 액세스합니다. Logger는 내부적으로 여러 모듈로 쪼개질 수 있지만, 우리에겐 모든 것이 Logger 모듈의 단순하고 잘 정의된 public API를 통해 노출됩니다.

Phoenix 프로젝트는 Elixir 라이브러리나 프로젝트처럼 구성됩니다. 코드를 컨텍스트들로 분할했습니다. 컨텍스트는 게시물 및 댓글과 같은 관련 function을 그룹화하며 종종 비즈니스 요구사항에 따라 데이터 액세스 및 데이터 유효성 검사와 같은 패턴을 캡슐화합니다. 컨텍스트를 사용하여 시스템을 분리 가능하고 독립적인 부분으로 분리합니다. 이러한 API는 복잡성과 구현 세부 사항을 숨기면서 중요한 logical concept을 노출시킵니다.

이러한 목표를 염두에 두고 단기적으로 하드코딩된 데이터를 바탕으로 컨텍스트를 작성합니다. 이 인터페이스를 통해 앱을 빌드할 때 앱을 신속하게 테스트하고 간단한 데이터 전용 struct를 사용하여 컨트롤러, 뷰 및 템플릿을 테스트할 수 있습니다. 나중에 우리는 하드코딩된 구현을 완전한 데이터베이스 기반 Ecto repository로 교체할 수 있으며 public interface는 변경되지 않을 것입니다.

요약: 다들 그렇듯이 공개 인터페이스와 비즈니스 로직을 잘 분리하고자 하고 여기선 컨텍스트가 그 인터페이스 역할을 한다. 일단 디비 없이 데이터 뱉도록 할 것.

사용자 기능이 어디에 있어야 하는지 생각해보죠. 사용자 계정은 rumbl의 핵심 부분이므로 이러한 문제를 그룹화하기 위해 Accounts라는 모듈을 만들어 보겠습니다. rumbl이 커짐에 따라 인증 또는 비밀번호 재설정과 같은 관련 function들로 해당 코드를 확장할 수 있습니다.

이제 우린 Account context가 필요하다는걸 알고있으므로 그 조각들을 점점 맞춰갑시다. lib/rumbl/accounts/user.ex 에 새 파일을 만들고 다음과 같이 입력합니다.

#rumbl/lib/rumbl/accounts/user.ex
defmodule Rumbl.Accounts.User do
 defstruct [:id, :name, :username]
end

User 모듈이 Elixir의 구조화된 데이터를 위한 주요 추상화인 Elixir struct를 정의합니다.

  • Elixir Structs

사용자를 배치하는 Accounts context를 정의해봅시다. 사용자 계정 fetching을 허용하는 몇 가지 함수를 추가합니다. lib/rumbl/accounts.ex 에 새 파일을 만들고 다음을 입력합니다.

# rumbl/lib/rumbl/accounts.ex
defmodule Rumbl.Accounts do
 @moduledoc """
 The Accounts context
 """
 alias Rumbl.Accounts.User

 def list_users do
  [
   %User{id: "1", name: "Jose", username: "josevalim", password: "elixir"},
   %User{id: "2", name: "Bruce", username: "redrapids", password: "7langs"},
   %User{id: "3", name: "Chris", username: "chrismccord", password: "phx"}
  ]
 end

 def get_user(id) do
  Enum.find(list_users(), fn map -> map.id == id end)
 end

 def get_user_by(params) do
  Enum.find(list_users(), fn map ->
   Enum.all?(params, fn {key, val} -> Map.get(map, key) == val end)
  end)
 end
end

새 모듈에서는 사용자 구조체 목록을 관리할 것으로 예상되는 일반적인 기능중 일부를 정의했습니다.

list_users() 함수는 시스템의 모든 사용자 구조체 목록을 반환합니다. 마찬가지로 get_user 및 get_user_by함수는 시스템에서 id 또는 attributes 목록과 일치하는 단일 사용자를 가져옵니다.

이제 우리의 앱은 시스템에서 사용자 계정을 가져오는 인터페이스가 있습니다. Caller는 데이터베이스가 아닌 하드코딩된 데이터를 반환해준다는 사실을 알 수 없습니다. 더 큰 앱 설계를 구축하면서 나중에 데이터 스토리지를 보다 정교하게 만들 수 있습니다.

돌아가는지 확인해보자. iex -S mix로 콘솔에서 실행해보기.

iex> alias Rumbl.Accounts
iex> alias Rumbl.Accounts.User
iex> Accounts.list_users()
[
%Rumbl.Accounts.User{ id: "1",
    name: "José",
    username: "josevalim"
  },
%Rumbl.Accounts.User{ id: "2",
    name: "Bruce",
    username: "redrapids"
  },
%Rumbl.Accounts.User{ id: "3",
    name: "Chris",
    username: "chrismccord"
  }
]

iex> Accounts.get_user("1")
%Rumbl.Accounts.User{
  id: "1",
  name: "José",
  username: "josevalim"
}

iex> Accounts.get_user_by(name: "Bruce")
%Rumbl.Accounts.User{
  id: "2",
  name: "Bruce",
  username: "redrapids"
}

제대로 작동하는 사용자 계정 시스템을 가졌습니다. 컨트롤러는 제대로 작동할 것입니다. 이제 사용자 계정이 생겼으므로 fetch해서 렌더링 하는 부분으로 넘어갑니다.

컨트롤러 작성

이미 이전에 간단한 컨트롤러를 만들었으므로 기본은 알 것입니다. 이 시점에서 사실 사용자가 필요로 하는 모든 route를 자동으로 만들 수 있지만, 우리는 “뛰지마세요!” 라고 외치는 따분한 안전요원 “직접 해보세요!”라고 말하는 선생님 역할을 수행해 볼 것입니다. 단일 route의 작동 방식을 이해하면 나중에 강력한 shortcut을 더 쉽게 탐색할 수 있습니다. 구체적으로 두 개의 route가 필요합니다. UserController.index는 user의 list를 보여주고, UserController.show는 단일 사용자를 표시합니다. 늘 그랬듯 router.ex에 route를 작성할 것입니다.

# rumbl/lib/rumbl_web/router.ex
scope "/", Rumbl do
 pipe through :browser
 get "/users",      UserController, :index
 get "/users/:id",  UserController, :show
 get "/",           PageController, :index
end

두 개의 새로운 경로와 루트에 대한 기본 경로, 두 새로운 route는 :show:index action과 함께 아직 존재하지 않는 새로운 UserController를 사용합니다. 이 action들을 위한 이름과 URL들은 무작위가 아닙니다. :show, :index, :new, :create, :edit, :update, :delete action들은 모두 Phoenix에서 자주 쓰이는 것들입니다. 당장은 그냥 시키는대로 따르고 나중에 shortcut에 대해 배울 것입니다.

:index route를 자세히 살펴보자

 get "/users",      UserController, :index

이전에 get macro를 봤었습니다. route는 HTTP GET 요청을 /users와 같은 URL에 match하고 그 url을 UserController에 보내서 index action을 호출합니다. 그 route는 :index를 conn에 저장하고 적절한 pipeline을 호출합니다.

이제 서버를 재시작하고 browser가 http://localhost:4000/users 가리키도록 합시다. 컨트롤러가 없으니 다음 에러 메시지를 보게 될 것입니다.

UndefinedFunctionError at GET /users
undefined function: RumblWeb.UserController.init/1
 (module RumblWeb.UserController is not available)

컨트롤러를 만들고 Account context에서 users를 찾는 함수 하나를 만들겠습니다.

# rumbl/lib/rumbl_web/controllers/user_controller.ex
defmodule RumblWeb.UserController do
 use RumblWeb, :controller

 alias Rumbl.Accounts

 def index(conn, _params) do
  users = Accounts.list_users()
  render conn, "index.html", users: users
 end
end

코드를 나눠서 보자. 파일 상단에는 module을 정의하고 :controller API를 쓸 것이라고 하는 ceremony가 조금 있습니다. 지금 action은 index 뿐입니다.

users page에 접속해보면, 여전히 에러가 뜨지만 error message가 바뀌어있습니다.

UndefinedFunctionError at GET /users
undefined function: RumblWeb.UserView.render/2
 (module RumblWeb.UserView is not available)

뷰 작성

이 과정은 이전에 한번 해보았습니다. 처음 해볼 때 Hello, World 스타일로 컨트롤러, 뷰, 템플릿 하나씩 있는 기능을 만들어 보았었죠. 이제 약속했던 대로 더 자세한 설명을 들어가보죠. 상당수 프레임워크에서 view와 template은 동의어로 사용됩니다. 사용자는 컨트롤러가 한 task를 완료하면, 뷰가 어떤식으로든 렌더링한다는 것만 알면 충분합니다.

피닉스에서는 이 용어가 좀더 명시적입니다. view는 데이터를 최종 사용자가 HTML 또는 JSON과 같이 사용할 형식으로 변환하는 렌더링 함수들을 포함하는 모듈입니다. 다른 Elixir 함수와 똑같이 쓸 수 있습니다. 그 렌더링 함수들은 템플릿들로부터 정의될수 있습니다. 템플릿은 그 모듈의 함수인데, raw markup 언어와 (substitutions와 loops를 처리하기 위한) embeded Elixir 코드를 포함한 파일로부터 컴파일됩니다. view와 template 개념을 분리하면 원시 함수, embeded Elixir 엔진 또는 기타 템플릿 엔진을 사용하여 원하는 방식으로 데이터를 쉽게 렌더링 할 수 있습니다.

요약하자면, 뷰는 렌더링을 담당하는 모듈. 템플릿은 정적 markup과 native코드가 응답 페이지를 작성하고 함수로 컴파일 할 수 있는 웹 페이지 또는 fragments.

피닉스 쓰면서 결국 두가지 다 함수로 컴파일 할것입니다. view를 작성해보죠.

#rumbl/lib/rumbl_web/views/user_view.ex
defmodule RumblWeb.UserView do
 use RumblWeb, :view
 alias Rumbl.Accounts

 def first_name(%Accounts.User{name: name}) do
  name
  |> String.split(" ")
  |> Enum.at(0)
 end
end

사용자의 name 필드에서 성을 parse하는 간단한 first_name 함수를 추가했습니다. 다음은 template에 user 디렉토리를 추가하고 새로운 index template을 만들죠

# rumbl/lib/rumbl_web/templates/user/index.eex
<h1>Listing Users</h1>
<table>
  <%= for user <- @users do %>
  <tr>
    <td><b><%= first_name(user) %></b> (<%= user.id %>)</td>
    <td><%= link "View", to: Routes.user_path(@conn, :show, user.id) %></td>
  </tr>
  <% end %>
</table>

거의 HTML 마크업이고 약간의 엘릭서 혼재. 실행 시 Phoenix는 이 템플릿을 함수로 변환하지만, 이렇게 생각해보죠. EEx는 <%= %> 태그 안에 있는 엘릭서 코드를 실행하여 결과를 템플릿에 실행합니다. EEx는 <% %> 태그 내에 있는 코드는 결과를 주입하지 않고 코드를 evaluate합니다. 웬만하면 view에서 부수 효과 없이 코드를 사용하기위해 노력할 것이므로 여기서는 대부분 <%= %> 형식을 쓸것입니다.

for user <- @users 표현식은 user를 순회하며 각 user가 do block 내의 템플릿 코드를 사용하여 렌더링하고 결과를 템플릿으로 roll up합니다.

각 user는 map입니다. name 필드와 id필드 및 link 하나를 렌더링했습니다. 그 link는 helper function에서 왔습니다.

  • Chris says:

Helper 사용

저 link 함수는 놀라운 양의 punch를 작은 패키지 안에 담습니다. Phoenix helpers는 일반적인 HTML구조를 view에 놓을수 있는 편리한 방법을 제공합니다. Helper는 특별할 것 없는 그냥 엘릭서 함수들입니다. 예를 들어, IEx에서 직접 helper들을 호출할 수 있습니다.

$ iex -S mix
iex> Phoenix.HTML.Link.link("Home", to: "/")
{:safe, [60, "a", [[32, "href", 61, 34, "/", 34]], 62, "Home", 60, 47, "a", 62]}

반환값이 조금 이상해보일 수 있습니다. 튜플에 :safe 다음에 특이한 값 목록이 나왔다. 이 목록을 I/O 목록이라고 합니다. I/O 목록은 단순히 값을 소켓에 쓰는 것과 같이 데이터를 I/O에 효율적으로 사용할 수 있는 값 목록입니다.

Phoenix.HTML.safe_to_string/1 을 호출하여 사람이 읽기 쉽게 나타내보면,

iex> Phoenix.HTML.Link.link("Home", to: "/") |> Phoenix.HTML.safe_to_string()
"<a href=\"/\">Home</a>"

link 함수의 두번째 인수는 keyword list이며 대상을 지정하는 to: 인자가 있습니다. link target을 지정하기 위해 :show route에 자동으로 생성된 path를 사용할 것입니다. 이제 목록에 repository에서 가져온 세 명의 사용자가 있는걸 볼 수 있습니다.

아마 지금쯤 HTML helper들이 어디서 왔는지 궁금해할수도 있습니다. 각 view의 윗부분에는 use RumblWeb, :view 라는 정의를 찾아볼수 있는데, 이 코드 스니펫은 view 모듈을 설정하고 필요한 모든 기능을 가져오는 역할을 합니다. lib/rumbl_web.ex 파일을 열어서 각 view에 무엇이 import되고 있는지 살펴보죠.

 def view do
    quote do
      use Phoenix.View,
        root: "lib/rumbl_web/templates",
        namespace: RumblWeb

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]

      # Include shared imports and aliases for views
      unquote(view_helpers())
    end
  end

 defp view_helpers do
    quote do
      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      # Import basic rendering functionality (render, render_layout, etc)
      import Phoenix.View

      import RumblWeb.ErrorHelpers
      import RumblWeb.Gettext
      alias RumblWeb.Router.Helpers, as: Routes
    end
  end

Phoenix.HTML은 링크 생성에서 form까지 뷰의 HTML 기능을 담당. HTML 안전성도 제공. Phoenix.HTML 함수에 의해 생성된 마크업만 안전하다고 간주되기에 XSS 공격으로부터 안전. 그래서 link 함수가 튜플을 반환하는 것인데, first가 second content가 안전한지 알려주는 :safe atom.

더 HTML helper를 배우고 싶으면 Phoenix.HTML document 읽어보기.

rumbl_web.ex 파일은 사용자 정의 함수들을 놓기에 좋은 장소가 아니다. 이 파일은 간결하고 쉽게 이해되도록 유지하고자 한다. 예를 들어, view 함수의 내용들은 모든 view 각각으로 macro-expanded 될것이다. 따라서 rumbl_web.ex에서는 사용자 정의된 함수들을 쓸 때 되도록 import 구문을 사용하라.

지금까지 많은 발전이 있었다. 이제 목표했던 actions의 완성을 위해 action 하나와 그에 맞는 템플릿 하나씩 더 추가해보자.

한 User 보여주기

사용자 목록을 보여주는 코드를 작성했으니 이제 단일 사용자를 보여주는 작업을 할 수 있습니다. 만들어두었던 route 다시 보면,

get "/users/:id", UserController, :show

/users/:id 요청을 보면, id는 inbound URL부분이고, router는 conn에 우리에게 필요한 최소 두가지를 추가하는데, 하나는 :id, 하나는 action의 이름인 :show . 그다음에 이제 router는 파이프라인의 plugs를 호출하고 UserController를 호출한다. 이 요청으로 한 유저를 보여주려면, 그에맞는 controller action이 필요하니 넣어주자.

# rumbl/lib/rumbl_web/controllers/user_controller.ex
def show(conn, %{"id" => id}) do
 user = Accounts.get_user(id)
 render(conn, "show.html", user: user)
end

이제 plug가 inbound connparams부분을 분리하는 이유를 알 수 있다. params을 사용하여 action에 필요한 개별 elements를 추출할 수 있다. 이 경우 “id” key와 일치하는 id 변수를 찾고, Repo에서 record를 찾는데 사용한다.

지금 localhost:4000/user/1 로 요청해도 아직 템플릿이 없다. 템플릿 추가해주자.

# rumbl/lib/rumbl_web/templates/user/show.html.eex
<h1>Showing User</h1>
<b><%= first_name(@user) %></b> (<%= @user.id %>)

Naming Conventions

Phoenix는 컨트롤러에서 템플릿을 렌더링 할 때 컨트롤러 모듈 이름 RumblWeb.UserController에서 뷰 모듈의 이름 RumblWeb.UserView를 유추한다. 뷰 모듈은 그들의 템플릿 위치를 뷰 모듈 이름으로 찾는다. RumblWeb.UserViewrumbl_web/templates/user/ 디렉토리를 찾는 식. Phoenix는 혼란스러운 pluralization 규칙과 naming 불일치를 피하기 위해 단일 이름들을 전체적으로 사용한다.

나중에 이러한 convention을 커스터마이징하는걸 설명할 것이다. 당장은 Phoenix가 하는대로 내버려두자. 필요하면 규칙은 어겨도 되지만, 현명한 사용자라면 지루한 의식은 생략하는게 좋다(?)

Nesting Templates

종종 템플릿 중복을 줄일 필요가 있다. 예를 들어 우리 템플릿들에는 user를 렌더링하는 공통부분이 있다. 공통부분을 user/user.html.eex로 분리하자

<b><%= first_name(@user) %></b> (<%= @user.id %>)

user/show.html.eex이 위 템플릿을 렌더링하도록 변경하자.

<h1>Showing User</h1>
<%= render "user.html", user: @user %>

user/index.html.eex 템플릿도 변경해야한다.

<tr>
 <td><%= render "user.html", user: user %></td>
 <td><%= link "View", to: Routes.user_path(@conn, :show, user.id) %></td>
</tr>

이쯤에서 Phoenix의 모든 view는 모듈이며 template은 그저 함수라는것을 강조하는 것이 좋겠다. 우리가 user.html.eex라는 템플릿을 추가하면 view는 파일 시스템에서 그 템플릿을 추출하여 view의 함수로 만든다. 그래서 먼저 view가 필요한 것. 이것을 iex에서 해보자

iex> user = Rumbl.Accounts.get_user("1")
%Rumbl.Accounts.User{...}
iex> view = RumblWeb.UserView.render("user.html", user: user)
{:safe, [[[[["" | "<strong>"] | "José"] | "</strong> ("] | "1"] | ")\n"]}
iex> Phoenix.HTML.safe_to_string(view)
"<strong>José</strong> (1)\n"

repository에서 사용자를 가져온 다음 템플릿을 직접 렌더링한다. Phoenix는 HTML safety라는 개념을 가지고 있기에 render는 튜플을 반환, :safe가 태그되고 content는 성능을 위해 list에 저장된다. Phoenix.HTML.safe_to_string을 호출해 이 안전하고 빠른 표현을 string으로 변환한다.

앱의 각 템플릿은 해당 view에서 render(template_name, assigns) 절이 된다. 따라서 템플릿 렌더링은 템플릿 이름의 패턴 매칭과 함수 실행의 조합이다. 렌더링 contract는 매우 간단하기 때문에 개발자가 view 템플릿에서 직접 렌더링 절을 정의하여 전체 템플릿을 건너뛰어버릴수도 있다. 이게 앱에서 오류가 있을때마다 Phoenix가 호출할 view인 RumblWeb.ErrorView에서 쓰이는 테크닉이다.

# rumbl/lib/rumbl_web/views/error_view.ex
def render("404.html", _assigns) do
 "Page not found"
end
def render("500.html", _assigns) do
 "Server internal error"
end

기본 생성된 error view는 template_not_found/2 콜백을 구현한다. 다음 RumblWeb.ErrorView의 action에서 볼 수 있다.

# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes # "Not Found".
def template_not_found(template, _assigns) do
 Phoenix.Controller.status_message_from_template(template)
end

view 자체를 정의하는 사용되는 Phoenix.View 모듈은 렌더링 된 템플릿을 한번에 문자열로 렌더링하고 변환하는 기능을 포함하여 뷰를 렌더링하는 기능도 제공한다.

iex> user = Rumbl.Accounts.get_user("1")
 %Rumbl.Accounts.User{...}
iex> Phoenix.View.render(RumblWeb.UserView, "user.html", user: user)
{:safe, [[[[["" | "<strong>"] | "José"] | "</strong> ("] | "1"] | ")\n"]}
iex> Phoenix.View.render_to_string(RumblWeb.UserView, "user.html", user: user)
"<strong>José</strong> (1)\n"

Phoenix.View 호출은 주어진 뷰에서 렌더링을 제공하고 템플릿을 가능한 레이아웃으로 래핑 하는 것과 같이 약간의 편의를 추가한다. 어떻게 하는지 알아보자.

Layouts

컨트롤러에서 render를 호출하면 원하는 뷰를 직접 렌더링 하는 대신 컨트롤러는 먼저 layout view를 렌더링한 다음 실제 템플릿을 사전 정의된 마크업으로 렌더링한다. 이를 통해 개발자는 반복작업 없이 모든 페이지에서 일관된 마크업을 제공해줄 수 있다.

레이아웃은 템플릿이 있는 보통의 view이므로 지금까지 했던 얻었던 지식들이 모두 적용된다. 특히 각 템플릿은 렌더링할 때 @view_module이나 @view_template과 같은 몇 가지 특수한 할당을 받는다. 다음과 같이 layout/app.html.eex에서 확인 가능

<main role="main" class="container">
 <p class="alert alert-info" role="alert">
  <%= get_flash(@conn, :info) %>
 </p>
 <p class="alert alert-danger" role="alert">
  <%= get_flash(@conn, :error) %>
 </p>
 <%= render @view_module, @view_template, assigns %>
</main>

render @view_module, @view_templates, assign 는 순수한 HTML이지만 HTML로 제한될 필요는 없다. 다른 템플릿과 마찬가지로, connection은 layout에서 @conn으로 쓸 수 있고 Phoenix의 다른 helper에 접근 가능. 컨트롤러에서 render를 호출하면 사실은 :layout 옵션이 기본으로 붙는다. 이를 통해 일반 render 함수 호출을 사용하여 레이아웃에서 컨트롤러 action에 대한 view 및 템플릿을 렌더링 할 수 있다.

기존 레이아웃을 조정하여 앱을 좀더 친숙하게 만들 수 있다. 여기에 CSS와 HTML을 많이 넣지 말고 자신만의 디자인을 만들어 보도록 할 것이다. 그렇게 하려면 layout/app.html.eex 에서 찾은 레이아웃을 원하는 레이아웃으로 바꾸라.

이제 막 끝냈다. 현재 우리의 성장하는 회사의 가치는 아마 학생 때 만든 나무 위 오두막 정도이다. 걱정 마시라, 빠르게 진행될 테니까. 당신이 생각했던 것보다 더 빠르게 깊이 들어갈 것이다.

요약

수행한 작업을 요약해보자.

  • 계정 작업에 관련된 모든 로직을 캡슐화한 첫 컨텍스트를 만들었습니다.
  • 각 요청에 대한 주요 제어 지점 역할을 하는 action들을 만들었습니다.
  • 템플릿을 렌더링하기 위해 존재하는 뷰를 만들었습니다.
  • 사용자를 위해 HTML을 생성하는 템플릿을 만들었습니다.
  • 템플릿에 사용되는 간단한 Phoenix 함수들인 helpers를 만들었습니다.
  • action의 html을 포함하는 html템플릿인 layout을 사용했습니다.

다음 챕터에서 context로 돌아가서, 단일 in-memory 리스트로 구성되었던 그것을 Ecto를 이용해 database로 대체합니다. 완료되면, 우리는 데이터베이스에서 사용자들을 읽고 새 사용자를 form으로 추가할 것입니다. 그 과정에서 상황에 따라 약간의 context이용한 선행 설계가 점점 커지는 feature set을 어떻게 감당하는지 살펴볼 것입니다.

Don’t stop now! Things are just getting interesting.