Elixir v1.20 변경 로그

Elixir v1.20 변경 로그 (Changelog)

이 릴리즈는 Erlang/OTP 27+가 필요하고 Erlang/OTP 29와 호환됩니다.

출처: Changelog for Elixir v1.20

본문

타입 시스템 개선

Elixir의 타입 시스템은 이제 모든 언어 구성을 이해하며, Elixir 표준 라이브러리와 여러분의 의존성에서 얻은 타입 정보를 사용해 여러분의 함수 정의에 대한 타입을 추론하고, 검증된 버그와 죽은 코드(dead code)를 찾을 수 있어요. 이는 절(clause) 전반의 타입 정제(type refinement), 발생 타입(occurrence typing), 맵 키와 도메인의 타입화 등의 일련의 개선을 통해 이루어졌습니다.

가드의 타입 추론

이 릴리즈는 가드(guard)의 추론도 수행합니다. 몇 가지 예를 볼게요:

def example(x, y) when is_list(x) and is_integer(y)

위 코드는 x가 리스트이고 y가 정수임을 올바르게 추론합니다.

def example({:ok, x} = y) when is_binary(x) or is_integer(x)

위 코드는 x가 binary 또는 정수이고, y가 첫 요소가 :ok이고 두 번째가 binary 또는 정수인 두 요소 튜플임을 추론해요.

def example(x) when is_map_key(x, :foo)

위 코드는 x:foo 키를 가진 맵(%{..., foo: dynamic()}로 표현)임을 추론합니다. 앞의 ...가 맵에 다른 키도 있을 수 있음을 나타낸다는 점을 기억하세요.

def example(x) when not is_map_key(x, :foo)

그리고 위 코드는 x:foo 키가 없다고 추론합니다(그래서 x.foo는 타입 위반을 일으키겠죠). 타입은 %{..., foo: not_set()}입니다.

데이터 구조의 크기를 단정하는 표현식도 있을 수 있어요:

def example(x) when tuple_size(x) < 3

Elixir는 튜플이 최대 두 요소라는 것을 올바르게 추적해, elem(x, 3)에 접근하면 타입 위반을 방출합니다. 즉 Elixir는 복잡한 가드를 보고 타입을 추론하며, (아직은) 타입 시그니처를 도입할 필요 없이 이 정보를 활용해 코드의 버그를 찾을 수 있어요.

함수 본문 전체의 타입 추론

Elixir는 또한 함수 본문 자체에 기반한 추론을 수행합니다. 다음 코드를 보세요:

def add_foo_and_bar(data) do
  data.foo + data.bar
end

Elixir는 이제 함수가 첫 인자로 map을 기대하고, 그 맵은 값이 integer() 또는 float().foo.bar 키를 가져야 한다고 추론합니다. 반환 타입도 integer() 또는 float()가 됩니다.

또 다른 예:

def sum_to_string(a, b) do
  Integer.to_string(a + b)
end

+ 연산자는 정수와 실수 모두에서 동작하지만, Elixir는 +의 결과가 정수를 기대하는 함수에 전달되므로 ab가 둘 다 정수여야 한다고 추론해요. 추론된 타입 정보는 이후 타입 검사 중에 가능한 타입 오류를 찾는 데 사용됩니다. 여러분의 의존성에서 추론된 타입도 여러분의 애플리케이션에 대한 더 정확한 타입을 추론하는 데 도움을 줍니다.

절 전반의 타입화

Elixir는 이제 이전 절에 기반해 주어진 절의 타입을 추론합니다. 예시를 볼게요:

case System.get_env("SOME_VAR") do
  nil -> :not_found
  value -> {:ok, String.upcase(value)}
end

System.get_env("SOME_VAR")nil 또는 binary()를 반환해요. 첫 번째 절이 nil에 매칭하므로, 타입 시스템은 value가 더 이상 nil이 될 수 없고 반드시 binary()만 되어야 한다는 것을 압니다. 이 덕분에 두 번째 절도 위반 없이 타입 검사를 통과합니다.

이런 절 전반의 타입 추론은 기존 코드베이스에서 중복 절과 죽은 코드를 찾는 데도 도움을 줍니다. Elixir v1.20은 또한 cond, case, with에 대한 발생 타입을 구현해, 각 절 안에서 더 정확한 타입을 제공합니다.

맵의 atom과 도메인 키 타입화

맵은 Elixir 타입 시스템에서 처음 구현한 데이터 구조 중 하나지만, 그때까지는 atom 키만 지원했어요. 추가 키가 있다면 그 키들은 단순히 dynamic()으로 표시됐죠. Elixir v1.20부터는 모든 가능한 도메인을 맵 키로 추적할 수 있습니다. 예를 들어 맵:

%{123 => "hello", 456.0 => :ok}

은 다음과 같은 타입을 갖습니다:

%{integer() => binary(), float() => :ok}

위처럼 도메인 키를 atom 키와 섞을 수도 있어서, 다음과 같은 결과를 얻을 수 있어요:

%{integer() => integer(), root: integer()}

이 시스템은 Giuseppe Castagna의 Typing Records, Maps, and Structs (2023)를 구현한 것입니다.

맵 연산의 타입화

Map 모듈의 대부분의 함수에 타입을 부여해, 타입 시스템이 모든 가능한 키 타입에 걸쳐 키가 어떻게 추가·업데이트·제거되는지 추적하게 했어요. 예를 들어 정확한 모양을 모르는 변수 map과 atom 키로 Map 함수를 호출한다고 상상해 보세요:

Map.put(map, :key, 123)
#=> returns type %{..., key: integer()}

Map.delete(map, :key)
#=> returns type %{..., key: not_set()}

보시다시피 키가 설정된 때와 제거된 때를 모두 추적합니다. Map.replace/3 같은 일부 연산은 키가 존재할 때만 교체하는데, 그것 역시 타입 시스템이 전파합니다:

Map.replace(map, :key, 123)
#=> returns type %{..., key: if_set(integer())}

즉 키가 존재한다면 정수 값으로 교체됐을 거라는 뜻이에요. 게다가 Map 모듈의 함수를 호출할 때 주어진 키가 맵에 결코 존재하지 않는다고 정적으로 증명되면 오류가 방출됩니다.

Map.fetch!/2, Map.pop!/2, Map.replace!/3, Map.update!/3 같은 bang 연산과 완전한 타입 추론을 결합하면, Elixir는 원하는 키에 대한 정보를 전파합니다. 이 모듈을 보세요:

defmodule User do
  def name(map), do: Map.fetch!(map, :name)
end

defmodule CallsUser do
  def calls_name do
    User.name(%{})
  end
end

위 코드에는 타입 위반이 있으며, 이제 타입 시스템이 그것을 잡아냅니다:

    warning: incompatible types given to User.name/1:

        User.name(%{})

    given types:

        %{name: not_set()}

    but expected one of:

        dynamic(%{..., name: term()})

    type warning found at:
    │
 16 │     User.name(%{})
    │         ~
    │
    └─ lib/calls_user.ex:7:5: CallsUser.calls_name/0

감사의 말

타입 시스템은 CNRSRemote의 파트너십 덕분에 가능해졌습니다. 현재 개발은 FreshaTidewave가 후원하고 있어요.

컴파일 타임 개선

Elixir v1.20은 컴파일 시간을 한 번 더 개선합니다. 특히 코어가 많은 애플리케이션에서 두드러져요. :module_definition이라는 새 컴파일러 옵션도 도입합니다. 이 옵션은 모듈 정의를 :compiled(기본값) 또는 :interpreted로 처리할지 정합니다. 이는 디스크에 쓰여지는 .beam 파일에는 영향을 주지 않고, defmodule 안의 내용이 어떻게 실행되는지만 영향을 줘요. :interpreted 모드를 사용하면 큰 프로젝트, 특히 코어 수가 많은 머신에서 더 나은 컴파일 시간을 얻을 수 있지만 몇 가지 단점이 있습니다:

  • 컴파일 중 오류의 스택트레이스가 덜 정확할 수 있음
  • defmodule 안의 익명 함수는 최대 20개 인자까지만 가질 수 있음. 문제가 된다면 맵이나 튜플로 데이터를 묶으면 됩니다. defmodule 안의 함수들 자체(예: def 안에 정의된 것들)는 여전히 최대 255개 인자를 가질 수 있어요

mix.exs에서 elixirc_options: [module_definition: :interpreted]로 설정하면 활성화할 수 있습니다.

패치 릴리즈 요약

  • v1.20.4 (2026-08-28) — 보안: List.to_string/1·List.to_charlist/1에 잘못된 charlist가 주어질 때 재귀를 피함 (CVE-2026-75758). 버그 수정: Base64 검증 오탐, String 그리스어 대문자화의 final sigma 처리, Task.yield_many/2:limit이 태스크 수를 초과할 때 무한 대기하는 문제 등.
  • v1.20.3 (2026-08-04) — 개선: Kernel.ParallelCompiler의 타입 체커 캐시 일괄 처리로 컴파일 시간 개선. 버그 수정: ++/2·send/2 타입 정밀도, and/2·or/2 타입 정제, Map.put/3 타입 검사 등 다수.
  • v1.20.2 (2026-06-23) — 개선: 컴파일러 프로파일링(profile: :time) 시 모듈별 타입 검사 시간 포함. 버그 수정: binary 컴프리헨션, __info__(:struct):required 키, protocol 구현 타입 경고 등 다수.
  • v1.20.1 (2026-06-09) — 보안: Version의 정수 컴포넌트를 14 십진 바이트로 제한 (CVE-2026-49762). 버그 수정: Calendar.strftime/2 너비 상한 1024, Code.require_file 파일 해제 보장 등.
  • v1.20.0 (2026-06-03) — 개선: Integer.ceil_div/2, Integer.popcount/1, IO.iodata_empty?/1, List.first!/1, List.last!/1, Process.get_label/1, Regex.import/1, Enum.min_max sorter, Code:dbg_callback·:module_definition: :interpreted 옵션, mix source, mix test --dry-run 등. 잠재적 호환성 변경: 문자열·주석·? 뒤의 raw CR 줄 끝 금지, require SomeModule의 컴파일 타임 확장 변경. 버그 수정 다수.

하드 폐기 (v1.20.0)

  • File.stream!(path, modes, lines_or_bytes)File.stream!(path, lines_or_bytes, modes) (파일)
  • 비트 패턴 안 크기 매칭은 이제 일관성을 위해 핀 연산자 필요, 예: <<x::size(^existing_var)>>
  • Kernel.ParallelCompiler.async/1Kernel.ParallelCompiler.pmap/2
  • Logger.*_backend 함수들 → 핸들러(:logger_backends 패키지 참고)
  • Logger.enable/1·Logger.disable/1Logger.put_process_level/2·Logger.delete_process_level/1
  • mix.exsxref: [exclude: ...]elixirc_options: [no_warn_undefined: ...]

v1.19 릴리즈의 CHANGELOG는 v1.19 브랜치에서 찾을 수 있어요.

더 알아보기

  • 이 릴리즈의 폐기 정책 상세는 Compatibility and deprecations 문서에서 확인하세요.
  • 타입 시스템에 대한 자세한 내용은 Elixir 공식 타입 시스템 가이드를 참고하세요.
  • 각 버전의 전체 변경 내역은 GitHub의 CHANGELOG 브랜치에서 볼 수 있어요.