Elm의 첫 10k LOC에서 배운 교훈

저는 지난 몇 달 동안 Elm으로 작성된 프론트엔드가 있는 개인 프로젝트를 진행해 왔습니다. 지금까지 모든 것이 잘 진행되고 있으며 프로젝트는 약 10,000줄의 코드입니다.

나는 지금까지 배웠고 공유하고 싶은 몇 가지 반복되는 패턴을 발견했습니다. 그래서 여기에 제가 배운 5가지가 있습니다.

1. 빈 목록과 문자열 디코딩



처음 프로젝트를 시작했을 때 다음과 같이 선언된 필드가 있는 많은 유형이 있었습니다.

type alias Something =
  { name : Maybe String
  , stuff : Maybe (List String)
  }


내 API에서 이러한 값을 디코딩하는 방법은 다음과 같습니다.

import Json.Decode as D
import Json.Decode.Pipeline exposing (optional)

D.succeed Something
  |> optional "name" (D.nullable D.string) Nothing
  |> optional "stuff" (D.nullable (D.list D.string)) Nothing


처음에는 괜찮았지만 잠시 후 name 필드가 빈 문자열인지 또는 stuff 필드가 빈 목록인지 확인하기 위해 업데이트 함수 내에서 논리를 반복하고 있다는 것을 알아차렸습니다. 값을 Nothing 또는 Just "" 대신 Just [] 로 변경하십시오.

그래서 잠시 후 디코딩 단계에서 빈 문자열과 빈 목록을 확인하는 것이 더 효율적이라고 생각하여 두 개의 디코더 도우미 함수 decodeNonEmptyStringdecodeNonEmptyList를 생각해 냈습니다.

decodeNonEmptyString : D.Decoder (Maybe String)
decodeNonEmptyString =
  D.andThen (notEmptyString >> D.succeed) D.string

decodeNonEmptyList : D.Decoder a -> D.Decoder (Maybe (List a))
decodeNonEmptyList l =
  D.andThen (notEmptyList >> D.succeed) (D.maybe (D.list l))

notEmptyList : Maybe (List a) -> Maybe (List a)
notEmptyList =
  Maybe.andThen (\l ->
    if List.isEmpty l then
      Nothing
    else
      Just l
  )

notEmptyString : String -> Maybe String
notEmptyString s =
  if String.isEmpty s then
      Nothing
  else
      Just a


이제 내 원래 디코더는 다음과 같은 새로운 유틸리티를 사용합니다.

D.succeed Something
  |> optional "name" decodeNonEmptyString Nothing
  |> optional "stuff" (decodeNonEmptyList D.string) Nothing


그리고 더 이상 업데이트 기능에서 공백을 확인할 필요가 없습니다 🎉

2. 플랫 패턴 매칭



이것은 정말 명백해 보일 수 있지만, 이렇게 하면 더 평평한 패턴 매칭 기능이 생긴다는 것을 깨닫는 데 시간이 걸렸습니다.
RD.WebData (Maybe (List Person)) 와 같은 유형이 있는 경우 원래 일치하는 방식은 다음과 같습니다.

case response of
  RD.NotAsked -> div [] [text "Not Asked"]
  RD.Loading -> div [] [text "Loading"]
  RD.Failure e -> div [] [text "Error"]
  RD.Success maybeData ->
    case maybeData of
      Nothing -> div [] [text "No data"]
      Just data -> div [] [text "Data!"]


그러나 나는 이것을 다음과 같이 2개의 개별적인 진술 대신에 하나의 case 진술로 병합할 수 있다는 것을 깨달았습니다.

case response of
  RD.NotAsked -> div [] [text "Not Asked"]
  RD.Loading -> div [] [text "Loading"]
  RD.Failure e -> div [] [text "Error"]
  RD.Success Nothing -> div [] [text "No data"]
  RD.Success (Just data) -> div [] [text "Data!"]


보다? 훨씬 더 멋져!

3. 래퍼 선택



이것은 쉽게 재사용할 수 있는 방법을 원했고select [] [] Elm의 기본 방법이 기대만큼 좋지 않다는 것을 발견했기 때문에 시작할 때 만든 첫 번째 추상화 중 하나입니다.

그래서 viewSelect 라는 작은 래퍼를 작성했습니다.

viewSelect : Bool -> (String -> msg) -> List Option -> Html msg
viewSelect disabled changeMsg options =
  select
    [ onChange changeMsg
    , Html.Styled.Attributes.disabled disabled
    ]
    (List.map
      (\opt ->
        option
          [ value opt.value
          , Html.Styled.Attributes.selected opt.selected
          , Html.Styled.Attributes.disabled opt.disabled
          ]
          [ text opt.text ]
      )
      options
    )

type alias Option =
  { text : String
  , value : String
  , selected : Bool
  , disabled : Bool
  }


onChange : (String -> msg) -> Html.Styled.Attribute msg
onChange tagger =
  on "change" (D.map tagger Html.Styled.Events.targetValue)


이제 네이티브viewSelect 대신 select를 사용하고 대신 Option 목록을 전달하면 됩니다. 사용자가 선택 상자를 변경할 때마다 changeMsg는 새 값으로 실행됩니다.

4. ChangeField 패턴



내 앱에는 많은 양식이 있으며 모든 양식은 내 모델 내부의 일부 필드를 수정해야 합니다. 원래 이 일을 시작한 방법은 다음과 같습니다.

type Msg
  = ChangeName String
  | ChangeAge String
  | ChangeHeight String
  | ChangeWeight String

update msg model =
  case msg of
    ChangeName name -> ({ model | name = name }, Cmd.none)
    ChangeAge age -> ({ model | age = age }, Cmd.none)
    ChangeHeight height -> ({ model | height = height }, Cmd.none)
    ChangeWeight weight -> ({ model | weight = weight }, Cmd.none)


이것도 처음에는 괜찮았지만 내 앱에는 양식이 많고 확장이 잘 되지 않았습니다. 그래서 모든 필드에 대해 새로운 메시지 유형을 만드는 대신 모든 필드를 업데이트하기 위해 하나의 메시지 유형을 만들었습니다. 그리고 다음과 같이 작동합니다.

type Msg = ChangeField (String -> Person -> Person) String

update msg model =
  case msg of
    ChangeField setter content -> (setter content model, Cmd.none)


이제 필드를 변경해야 할 때마다 ChangeName "Jason" 를 호출하는 대신 setName이 다음과 같은 곳에서 ChangeField setName "Jason"를 호출할 수 있습니다.

setName : String -> Person -> Person
setName content person =
    { person | name = content }


이를 통해 각 필드에 대해 개별 setter 함수를 만들고 String에서 필요한 모든 유형으로 데이터 조작을 수행할 수 있습니다.

일치시킬 수 있는 메시지 유형의 양을 줄여 업데이트 기능을 단순화하고 각 필드에 대한 데이터 조작 논리를 멋지게 분리합니다.

5. 포식스 래퍼



이것은 정말 간단합니다. 저는 ElmTime.Posix 유형을 앱 주변에 많이 사용하기 때문에 UI에서 Posix를 포맷, 조작 및 표시하는 기능이 필요했습니다. 이를 위해 몇 가지 도우미를 생각해 내 HumanTime 모듈이라고 부릅니다.

humanDateTime : Zone -> Posix -> String
humanDateTime z p =
  humanDate z p ++ " " ++ humanTime z p

humanTime : Zone -> Posix -> String
humanTime z p =
  humanHour z p ++ ":" ++ humanMinute z p

humanDate : Zone -> Posix -> String
humanDate z p =
  humanMonth z p ++ " " ++ humanDay z p ++ ", " ++ humanYear z p

humanYear : Zone -> Posix -> String
humanYear z =
  toYear z >> String.fromInt

humanDay : Zone -> Posix -> String
humanDay z =
  toDay z >> String.fromInt

humanHour : Zone -> Posix -> String
humanHour z =
  toHour z >> String.fromInt

humanMinute : Zone -> Posix -> String
humanMinute z =
  toMinute z
      >> (\m ->
            if m > 9 then
              String.fromInt m
            else
              "0" ++ String.fromInt m
         )

humanMonth : Zone -> Posix -> String
humanMonth z p =
  case toMonth z p of
    Jan -> "Jan"
    Feb -> "Feb"
    Mar -> "Mar"
    Apr -> "Apr"
    May -> "May"
    Jun -> "Jun"
    Jul -> "Jul"
    Aug -> "Aug"
    Sep -> "Sep"
    Oct -> "Oct"
    Nov -> "Nov"
    Dec -> "Dec"


그리고 그것이 지금까지입니다! 나는 내가 작성하는 다음 10k LOC에서 더 많은 것을 배우고 방금 언급한 이러한 작업을 수행하는 더 나은 방법을 알아낼 수 있을 것이라고 확신합니다.

Elm은 프론트엔드를 만들기 위한 놀라운 도구이며 개인 프로젝트를 위해 React나 다른 비기능적 프레임워크로 되돌아가는 것을 본 적이 없습니다.

이 기사가 마음에 들면 피드에 새 기사를 게시할 때마다 확인하십시오 ✨ 또는

좋은 웹페이지 즐겨찾기