cv.cm

코딩 · 프로그래밍 언어

초보자를 위한 Elixir 기초 튜토리얼

10분 분량

핵심 답변

Elixir는 Erlang 가상 머신(BEAM) 위에서 실행되는 동적 함수형 언어로, 확장 가능하고 장애에 강한 동시성 시스템을 위해 설계되었습니다. 핵심 개념은 불변 데이터, 패턴 매칭, 파이프 연산자, 메시지로 통신하는 경량 프로세스입니다. 이 페이지에서는 환경 설정, 타입, 패턴 매칭, 제어 흐름, 모듈과 함수, 컬렉션, 프로세스, 오류 처리를 설명합니다.

정규식 테스트 열기

Elixir란 무엇이며 어디에 쓰이는가

Elixir는 José Valim이 만들어 2012년에 처음 공개한 동적 함수형 언어입니다. Erlang의 가상 머신인 BEAM 위에서 실행되므로 Erlang의 경량 프로세스, 장애 허용, 분산 시스템 지원을 물려받고, 여기에 친숙한 문법, 매크로, 현대적인 도구를 더했습니다.

Elixir는 Phoenix 프레임워크를 이용한 웹 애플리케이션과 API, 채팅이나 실시간 대시보드 같은 실시간 기능(Phoenix LiveView), 메시징 시스템, 데이터 파이프라인, Nerves를 이용한 임베디드 장치, Nx를 이용한 머신러닝에 쓰입니다. 많은 동시 접속을 처리해야 하고 일부가 실패해도 계속 동작해야 하는 서비스에 기업들이 선택합니다.

Elixir 설치와 Hello, world 실행

elixir-lang.org에서 Elixir를 설치합니다(Erlang/OTP도 함께 설치되거나 필요합니다). 스크립트는 .exs 확장자를, 컴파일되는 프로젝트 파일은 .ex를 씁니다. iex는 대화형 셸을 시작합니다.

# hello.exs
IO.puts("Hello, world!")
elixir hello.exs
# Hello, world!
iex                      # interactive shell; type h(Enum.map) for help
mix new my_app           # create a new project with Mix, the build tool

주석은 #로 시작합니다. 많은 곳에서 함수 인자의 괄호를 생략할 수 있어서 IO.puts "hi"도 동작합니다.

기본 타입과 불변성

Elixir의 모든 데이터는 불변입니다. =로 이름을 다시 바인딩하면 새 값을 가리킬 뿐 기존 값을 바꾸지 않습니다. 문자열은 큰따옴표로 쓰는 UTF-8 바이너리이고, 아톰은 콜론으로 시작하는 이름 있는 상수입니다.

name = "Elixir"
year = 2012
IO.puts("#{name} since #{year}")    # Elixir since 2012
IO.puts(10 / 4)                     # 2.5 (always a float)
IO.puts(div(10, 4))                 # 2
IO.puts(rem(10, 4))                 # 2
IO.puts("ab" <> "cd")               # abcd (string concatenation)
IO.puts(String.length("héllo"))     # 5 (counts characters, not bytes)
IO.inspect(:ok)                     # :ok
IO.inspect(is_atom(true))           # true (booleans are atoms)
타입예시설명
integer42, 1_000_000임의 정밀도
float3.1464비트 배정밀도
atom:ok, :error상수. true, false, nil도 아톰입니다
string"hello"UTF-8 바이너리. #{}로 보간
list[1, 2, 3]연결 리스트
tuple{:ok, 1}고정 크기, 메모리에 연속 저장
map%{"a" => 1}키-값 저장소

패턴 매칭

Elixir에서 =는 매치 연산자입니다. 왼쪽이 패턴이며, 오른쪽이 그 패턴에 맞으면 패턴 안의 변수가 바인딩됩니다. 패턴 매칭은 대입, case, 함수 헤드, 오류 처리 등 모든 곳에서 쓰입니다.

{:ok, value} = {:ok, 42}
IO.puts(value)                         # 42

[first | rest] = [1, 2, 3]
IO.inspect({first, rest})              # {1, [2, 3]}

%{name: n} = %{name: "Ann", age: 31}
IO.puts(n)                             # Ann

result = {:error, :not_found}
message =
  case result do
    {:ok, v} -> "got #{v}"
    {:error, reason} -> "failed: #{reason}"
  end
IO.puts(message)                       # failed: not_found

x = 1
^x = 1                                 # pin operator: match the current value

패턴이 맞지 않으면 Elixir는 MatchError를 발생시킵니다. 핀 연산자 ^는 변수를 다시 바인딩하지 않고 그 변수의 기존 값을 사용하라는 뜻입니다.

제어 흐름

Elixir에는 case, cond, if/unless가 있으며 모두 값을 반환합니다. 카운터를 바꾸는 for/while 반복문은 없고, 재귀나 Enum 모듈, for 컴프리헨션을 사용합니다.

temp = 22
feel =
  cond do
    temp > 30 -> "hot"
    temp >= 15 -> "mild"
    true -> "cold"
  end
IO.puts(feel)                                   # mild

IO.puts(if(rem(temp, 2) == 0, do: "even", else: "odd"))  # even

squares = for n <- 1..5, rem(n, 2) == 1, do: n * n
IO.inspect(squares)                             # [1, 9, 25]

false와 nil만 거짓이고, 0과 ""를 포함한 나머지는 모두 참입니다.

함수, 모듈, 파이프 연산자

이름 있는 함수는 defmodule로 정의한 모듈 안에 있습니다. 함수는 여러 절을 가질 수 있으며, 패턴 매칭과 가드(when)로 선택됩니다. 익명 함수는 fn ... end로 쓰고 점(.)으로 호출합니다. 파이프 연산자 |>는 한 표현식의 결과를 다음 호출의 첫 번째 인자로 넘깁니다.

defmodule Math do
  def factorial(0), do: 1
  def factorial(n) when n > 0, do: n * factorial(n - 1)

  def sign(n) when n < 0, do: :negative
  def sign(0), do: :zero
  def sign(_n), do: :positive
end

IO.puts(Math.factorial(5))          # 120
IO.inspect(Math.sign(-3))           # :negative

double = fn x -> x * 2 end
IO.puts(double.(21))                # 42 (note the dot)

"  Elixir is fun  "
|> String.trim()
|> String.upcase()
|> String.split()
|> IO.inspect()                     # ["ELIXIR", "IS", "FUN"]

함수는 이름과 인자 개수(arity)로 식별하며, 문서에서는 String.split/1이나 Math.factorial/1처럼 씁니다.

컬렉션: 리스트, 맵, 키워드 리스트, Enum

Enum 모듈은 모든 열거 가능한 컬렉션에서 동작하며 map, filter, reduce, sort_by, group_by 등 많은 함수를 제공합니다. 맵은 주된 키-값 구조이고, [size: 10, color: :red] 같은 키워드 리스트는 옵션 전달에 자주 쓰입니다.

nums = [5, 3, 8, 1]
IO.inspect(Enum.sort(nums))                      # [1, 3, 5, 8]
IO.inspect(Enum.map(nums, &(&1 * 10)))           # [50, 30, 80, 10]
IO.inspect(Enum.filter(nums, fn n -> n > 2 end)) # [5, 3, 8]
IO.puts(Enum.reduce(nums, 0, &+/2))              # 17

user = %{name: "Ann", age: 31}
older = %{user | age: 32}                        # update an existing key
IO.puts(user.age)                                # 31 (unchanged)
IO.puts(older.age)                               # 32
with_city = Map.put(user, :city, "Oslo")
IO.puts(with_city.city)                          # Oslo
IO.inspect(Map.get(user, :email, "none"))        # "none"

캡처 연산자 &는 짧은 익명 함수를 만듭니다. &(&1 * 10)은 fn x -> x * 10 end와 같고, &+/2는 인자가 두 개인 + 함수를 가리킵니다.

프로세스와 동시성

Elixir의 대표 기능은 BEAM 프로세스입니다. 자체 메모리를 가진 매우 가볍고 격리된 실행 단위입니다. 프로세스는 메시지를 보내서만 통신하며, 한 대의 머신에서 수십만 개를 실행할 수 있습니다. 슈퍼바이저는 비정상 종료한 프로세스를 재시작하며, 이것이 Elixir의 장애 허용 방식인 let it crash의 기반입니다.

parent = self()

spawn(fn ->
  send(parent, {:result, 6 * 7})
end)

receive do
  {:result, value} -> IO.puts("Got #{value}")   # Got 42
after
  1_000 -> IO.puts("timeout")
end

tasks = for n <- 1..3, do: Task.async(fn -> n * n end)
IO.inspect(Task.await_many(tasks))               # [1, 4, 9]

실제 애플리케이션에서는 spawn을 직접 호출하는 일이 드뭅니다. Task, GenServer, Agent, Supervisor 같은 OTP 추상화를 사용합니다.

오류 처리

Elixir다운 방식은 예상되는 실패에 {:ok, value}나 {:error, reason} 같은 태그 튜플을 반환하고 case나 with로 처리하는 것입니다. File.read!/1처럼 이름이 !로 끝나는 함수는 대신 예외를 발생시킵니다. 예외는 try/rescue로 잡을 수 있지만 흔하지 않습니다.

case File.read("missing.txt") do
  {:ok, text} -> IO.puts(text)
  {:error, reason} -> IO.puts("Cannot read: #{reason}")   # Cannot read: enoent
end

with {a, ""} <- Integer.parse("40"),
     {b, ""} <- Integer.parse("2") do
  IO.puts(a + b)                                         # 42
else
  _ -> IO.puts("not a whole number")
end

try do
  String.to_integer("abc")
rescue
  e in ArgumentError -> IO.puts("Error: #{Exception.message(e)}")
end

with는 여러 매치를 연결하고, 맞지 않는 첫 번째 값에서 멈추며 그 값을 반환합니다. 깊게 중첩된 case 표현식을 피할 수 있습니다.

작은 완성 프로그램: 단어 빈도

이 스크립트는 텍스트의 단어를 세어 가장 흔한 세 개를 출력합니다. String과 Enum 함수로 구성한 전형적인 Elixir 파이프라인입니다.

defmodule WordFreq do
  def top(text, n) do
    text
    |> String.downcase()
    |> String.split(~r/[^a-z']+/, trim: true)
    |> Enum.frequencies()
    |> Enum.sort_by(fn {word, count} -> {-count, word} end)
    |> Enum.take(n)
  end
end

"The cat and the dog. The dog sat, and the cat ran."
|> WordFreq.top(3)
|> Enum.each(fn {word, count} -> IO.puts("#{word}: #{count}") end)
# the: 4
# and: 2
# cat: 2

다음 단계

  • hexdocs.pm의 공식 Getting Started 가이드와 elixir-lang.org 웹사이트.
  • Elixir와 모든 Hex 패키지의 문서가 공개되는 HexDocs.
  • 웹 개발을 위한 Phoenix 프레임워크 가이드.
  • 무료 커뮤니티 커리큘럼인 Elixir School과 연습용 Exercism의 Elixir 트랙.

자주 묻는 질문

Elixir와 Erlang의 차이는 무엇인가요?

둘 다 같은 BEAM 가상 머신에서 실행되며 서로의 코드를 호출할 수 있습니다. Elixir는 Ruby에서 영감을 받은 문법, 매크로, 파이프 연산자, Mix와 Hex 같은 도구를 더했고, Erlang은 OTP가 만들어진 더 오래된 언어입니다.

Elixir는 웹 개발에 좋은가요?

네. Phoenix 프레임워크는 빠르고 많은 동시 접속을 처리하며, LiveView를 쓰면 서버에서 렌더링한 HTML과 적은 JavaScript로 인터랙티브한 페이지를 만들 수 있습니다.

Elixir는 정적 타입 언어인가요?

Elixir는 동적 타입 언어입니다. 최근 버전에서는 컴파일러에 집합론 기반의 점진적 타입 시스템을 추가하고 있으며, 많은 팀이 타입 스펙과 함께 Dialyzer를 사용해 타입 오류를 찾습니다.

Elixir의 |>는 무슨 뜻인가요?

|>는 파이프 연산자입니다. x |> f(y)는 f(x, y)와 같으므로, 변환 과정을 위에서 아래로 읽을 수 있습니다.

.ex와 .exs 파일은 무엇인가요?

.ex 파일은 프로젝트의 일부로 컴파일되고, .exs 파일은 실행할 때 해석되는 스크립트로 테스트와 설정에 주로 쓰입니다.

코딩 튜토리얼 전체