Posts
design-system
일관된 디자인 시스템 API를 위한 가이드라인 작성하기 (2부)

들어가기 전에

2부를 이어서 작성하겠다고 한지 무려 5달이 지나버렸다… 회사에서 다같이 목표 하나를 바라보며 야근 신기록도 세우면서 달리느라 정신이 없었는데, 이제 차근차근 정리해보려고한다!

지난 글(일관된 디자인 시스템 API를 위한 가이드라인 작성하기-(1) 컴포넌트 합성 (opens in a new tab) )에서, 컴포넌트 합성과 컴포넌트 설계에 대해 주로 이야기했다. 이번에는 컴포넌트의 프롭 네이밍과 설계에 대한 가이드라인을 어떻게 작성했는지 이야기해보려고 한다. 물론 이 또한 대부분 MUI API 디자인 가이드라인을 많이 참고했다.

가장 어려운 것, 네이밍!

API design is hard because you can make it seem simple but it's actually deceptively complex, or make it actually simple but seem complex. @sebmarkbage (opens in a new tab)

mui api design approach 문서 가장 상단에 인용된 말인데, 뼈 맞도록 공감된다.

누가 보아도 빠르게 그 의미를 파악할 수 있으면서, 길지 않은 간단한 이름을 짓고 싶지만. 막상 그 변수나, prop이하는 일이 복잡한 경우 그 의미를 추상적으로 혹은 가장 핵심적으로 담을 수 있는 이름을 찾기란 매우 어렵다.

몇 년 전 개발을 처음 시작하던 즈음 *‘개발자가 가장 어려워하는 것이 변수명 짓기’*라는 말을 믿지 못했었는데, 이제는 꽤나 공감한다. 왜 우리는 이름을 짓는 것을 이토록 어려워하는 걸까?

그 이유는 우리가 네이밍을 할 때 신경써야할 암묵적인 기준들이 있기 마련인데, 그것을 모두 만족시키는 simple한 이름을 찾아야하기 때문이 아닐까?

네이밍 할 때 고려하는 점들

  • 의미 전달과 간결성: 이름을 통해 해당 변수, 함수가 어떤 역할을 하는지 길지 않은 단어 조합으로 명확하게 목적과 기능을 전달해야함

  • 일관성: 다른 비슷한 케이스에서 사용하는 이름을 참고하여 일관된 네이밍 규칙을 만들거나, 혹은 존재하지 않는 네이밍 규칙을 유추해서(..) 일관성을 유지

  • 추상적 ↔ 구체적: 너무 구체적이어도 verbose하고, 오히려 가독성을 떨어트리지만, 반대로 너무 추상적이어도 해당 변수 혹은 함수의 역할을 파악하기 어려움

  • 예측 불가능 : 지금 작성하는 코드가 나중에 어떻게 변하게 될지 모르기 때문에 최대한 유연하게 확장할 수 있는 구조와 네이밍을 사용하고 싶음

딱히 개발하면서 이렇게 요목 조목 따지면서 왜 네이밍이 이렇게나 어려운가, 생각해본 적은 없지만 모두 개발하면서 고민했을 부분이라고 생각한다. 예를 들어 ‘함수의 역할을 벗어난 일을 하면 안된다’는 생각에, 함수를 나누어서 각각 함수의 이름을 지으려고 할 때 너무 구체적으로 지을 수 밖에 없었던 경우 ‘과연 구체적으로 하는 일을 정확히 명시하는 것이 좋은 이름인가’ 고민하게 되었던 적도 있을 것이다.

디자인 시스템 컴포넌트의 경우, 이러한 네이밍은 사용자의 사용성뿐 아니라, 시스템을 아우르는 컨셉과도 직결되는 부분이기 때문에 하나의 이름을 짓는 것의 무게가 더 큰 것 같다고 생각한다.

특히, 사용자가 굳이 문서를 보면서 학습하지 아도 ‘이런 게 있을 것 같은데..’하는 기대에 부합할 수 있도록 일관된, 예측가능한 네이밍을 사용하는게 사용자의 러닝커브를 효과적으로 줄일 수 있는 가장 좋은 방법이다.

혹시라도 확장성을 고려하지 못한, 혹은 역할을 잘 나타내지 못한 이름을 사용하여 나중에 바뀌게 된다면 **‘재학습비용’**이 생기게 된다. - 나는 이게 처음 학습할 때 생기는 비용보다 더 큰 비용이 생긴다고 생각한다. 처음 사용자가 머릿속에 새겨두었던 개념이나 위계를 완전히 지우고 새로 쌓아야하기 때문이다.

물론 이 부분은 컴포넌트 prop뿐 아니라, 디자인 시스템에서 사용되는 개념을 정하는 것과 가깝기도하다. 이와 관련해서는

  • 확장성 있는 이름인가?
  • 사용처에 국한된 이름은 아닌가?
  • 같은 레벨에 있는 속성의 이름과 align 되는가?
  • 서로 구분되는 이름인가? (직관적이면 좋으나 항상 직관적일 순 없음)
  • 이름을 보고 사용처 or 역할 or 위계를 충분히 유추가능한가?

등을 고려하게된다.

서두가 너무 길었는데, 사실 가이드라인에는 위의 모든 고민들을 해결해줄 정답은 없다.

가이드라인은 가이드일뿐.. 정답이 아니며 많은 개발자들의 고민을 해결해줄 수도 없다.

위와 같은 사항들을 고려하면 좋다는 것을 말하고 싶고, 다음은 컴포넌트 인터페이스 설계 가이드라인 일부를 가져왔다.

boolean prop은 default가 false가 되도록 네이밍한다.

// need consideration :(
interface ButtonProps {
  enabled: boolean;
}
 
// recommended :)
interface ButtonProps {
  disabled: boolean;
}

show___, enable___ , hide___, disable___,등… boolean 타입의 프롭은 긍정 혹은 부정의 의미를 가질 수 있으며. 이름에 담긴 긍정/부정의 의미에 따라 같은 책임을 갖는 프롭이더라도 네이밍에 따라 default value가 달라진다. 위의 예시는 아주 간단하지만, 가끔 enable ↔ disable과 같이 딱 맞아떨어지게 네이밍이 되지 않을 때도 많다.

예를 들어 mui Dialog는 keepMounted 라는 prop을 받는다. 즉, 기본적으로 openfalse 가 되어 Dialog가 닫힐 때 내부 요소들이 unmount되는 것이 기본 behavior이다.

반면, Grow, Collapse와 같은 Transition 컴포넌트들은 unmountOnExit prop을 사용하는데, 이는 반대로 transition이 종료되어 화면에서 가려졌음에도 mount되어있는 것이 기본 behavior이기 때문이다.

💡

keepMountedunmountOnExit

하지만 이 두 개의 프롭은 사실 같은 종류의 역할을 갖는다는 걸 알 수 있다. 화면에서 사라졌을 때 계속 mount 시킬지, unmount 시킬지.

일관성이 중요한 디자인 시스템에서 왜 같은 역할을 하는 프롭을 이렇게나 다른 이름으로 사용중일까? 그 이유는 위에서 눈치 챗듯이, 바로 위의 두 가지 컴포넌트에서 행해지는 기본 액션이 다르고, 이에 따라 사용되는 boolean 프롭의 긍정/부정의 의미가 뒤바뀌게 되기 때문이다. 즉, 언제나 boolean 프롭은 default 값이 false 가 되도록 설계하기 위함이다.

그렇다면 왜 default 값이 false여야 하나?

이와 관련하여 불편함을 느껴본 적이 있는 개발자라면 굳이 설명하지 않아도 알겠지만, 자주 쓰이는 액션, 스타일, 포맷은 사용자가 직접 추가적인 옵션을 넣지 않더라도 기본적으로 제공되도록 동작해야 불편함을 최소화할 수 있다. 예를 들어, 만약 반대로 이미 팀 내에서 컨벤션으로 정해진 형식을 사용하기 위해 특정 hook을 사용할 때마다 매번 추가 옵션을 넣어줘야한다면? 어떤 누군가는 그 특정 옵션을 빼먹고 원하는 결과가 나오지 않아서 당황할 수 있다.

generateResponseType(response, { camelize: true }) // before
generateResponseType(response, { decamelize: false }) // after

위의 예제는 실제 내가 겪었던 사례이다. 대략 위와같이 서버에서 제공되는 websocket response의 타입을 자동 생성해주는 함수가 있었다. 그런데 정말 이상하게도, respone type이 자꾸만 snake_case로 생성이 되는게 아닌가! 헤매다가, 뒤늦게 camelize 라는 옵션을 추가할 수 있음을 알게되었는데, 프론트에선 항상 camelCase를 사용하기 때문에 이 옵션을 true로 넘겨야 한다는 걸 전혀 생각지 못했다.

디버깅 후, 또 헷갈릴 일이 생기지 않게 기본값이 false가 되도록 해당 함수에선 기본적으로 camelize를 하도록 기본 액션을 바꾸고, camelize 대신 decamelize 옵션을 추가하도록 변경했다. 이렇게 해야만 특별히 프론트에서 (그럴일은 잘 없겠지만) snake_case를 써야할 일이 생겼을 때만, 추가적으로 { decamelize: false } 를 넘기면 되며, 그 외에는 옵션을 넘기지 않아도 된다.

정리하면, 사용자가 예측할 수 있어야한다.

우리는 기본적으로 Button을 마크업할 때 기본적으로 활성화된 상태를 기대하는데, <Button enabled /> 처럼 매번 enabled 프롭을 추가해주어야 한다면 , 프로그래밍적으로 기본 상태는 disabled 로 되어있는 것이기 때문에 자연스럽지 못함.

컴포넌트나 함수의 기본 동작은 "가장 단순한 형태"로 작성할 수 있어야 하며, 또한 개발자들도 “가장 단순한 형태”가 기본동작이라고 예상함.

enum vs. boolean prop

<Button variant="outlined" />
<Button variant="contained" />
<Button outlined /> // default: contained style

color 프롭과 같이 이미 선택지가 primary, secondary, success, warning... 처럼 많은 경우는 헷갈릴 일이 없겠지만, 두 가지의 선택지만 있는 경우는 프롭을 boolean 형식으로 해야할지, 혹은 열거형으로 설계해야할지 고민이 될 수 있다.

위의 예시는 버튼 컴포넌트의 전반적 형태나 강조 위계를 달리하는 variant 프롭을 각각 열거형, boolean형으로 설계한 경우다.만약 Button 컴포넌트가 오직 “outlined”와 “contained” 형태만 가진다면, 두번째와 같이 boolean 형식으로 설계하여도 괜찮다고 생각할 것이다. 이때 어떤 것이 기본 스타일인지에 따라 커스텀을위해 추가적으로 넘겨야하는 프롭이름이 결정된다. → contained가 기본 스타일이라면, outlined라는 프롭을 넘겨 형태를 outline만 있는 버튼으로 변경한다.

하지만 이때 만약, 디자인팀에서 버튼의 추가 커스텀 니즈가 생겨 형태를 하나 더 추가하고 싶다고 할 수 있다. muted 라는 형태가 추가되어야 한다면, 당연한 얘기지만 mutedboolean 프롭으로 추가해선 안된다.

<Button outlined />
<Button muted /> // X
<Button outlined muted /> // 서로 배타적인 관계가 아님

outlinedmuted 는 동시에 적용될 수 없기 때문이다.

사실, 위의 예제에선 조금만 생각해봐도 첫번째와 같이 열거형으로 설계하는 것이 좋다는 것을 눈치챌 것이다. “추가적인 디자인 니즈”는 조금 과장해서 말하자면 항상 생기기 마련이고, 디자인 시스템에 “완성”이란 없으며, 프로덕트가 발전하고 다양한 케이스가 생겨남에 따라 다양한 커스텀 니즈가 생기기 마련이기 때문이다.

따라서 color , variant , weight스타일을 조정하는 프롭의 경우는 항상 확장 가능성을 염두에 두고, 지금 당장 선택지가 두개라고 하더라도 되도록 열거형으로 설계하는 것이 안전하다고 생각한다. 반면 일반적으로 서로 대치되는 개념을 컴포넌트에 적용하는 경우는 boolean 프롭으로 설계해도 괜찮다고 생각한다.

  • direction(vertical ↔ horizontal or row ↔ column)
  • disabled ↔ enabled
  • fullWidth ↔ default

일관된 용어 사용하기

당연한 이야기지만 다른 사용처라고 하더라도, 같은 역할을 하는 프롭을 추가하고자 한다면, 되도록 일관된 용어와 타입을 사용하는 것이 좋다.

material-ui를 사용하면서 가끔 특정 레이아웃을 세로 정렬로 만들기 위해서 column boolean 프롭을 넘기면될지, 혹은 direction="vertical" 형태로 넘겨야할지, 또는 orientation="vertical" 형태로 넘겨야할지 헷갈린 적이 꽤 많다. 그 이유는 컴포넌트마다 세로 정렬을 위해 이렇게 사용하는 프롭이 다르기 때문이다.

이와 관련해서 프롭 이름을 어떻게 통일할지에 대한 의논도 이루어진 적이 있다(https://github.com/mui/material-ui/discussions/33770 (opens in a new tab))

MUI 기반으로 만드는 시스템을 예로 들자면 특정 컴포넌트의 형태에서 외곽선을 강조하는 형태를 추가하고자 할 때 border 보다는 outlined 라는 프롭이 사용자에게 친숙한 이름일 것이다.

우리 디자인 시스템에서는 TableSortFilterLabel 이라는 복잡한 컴포넌트를 추가하여 리뷰한 적이 있었는데, (mateiral-ui의 TableSortLabel 을 확장하여 컬럼 필터 메뉴까지 제공하는 컴포넌트) 여기서도 이와 비슷한 논의를 한 적이 있다.

tableSortLabel.png

( 참고로 TableSortLabel은 눌렀을 때 사진과 같이 컬럼을 정렬할 수 있는 버튼이 나타난다.)

해당 컴포넌트는 정렬, 필터 기능까지 제공하면서 프롭이 매우 많아졌다. 프롭이 많아졌을 때 가장 주의해야할 점은, 그만큼, 아니 2배는 사용하기 어려워진다는 점이다. 사용자는 빠르게 각 프롭의 역할을 파악하기 어려워지기 때문에, 가능한 그 개수를 줄이는 것이 좋고(정말 필요한지 다시 한번 생각해보기), 각 프롭의 역할을 직관적으로 알 수 있는 네이밍이 중요해진다.

아래에서 보이듯 “order” “sort”와 같이 비슷한 의미의 단어이지만 하나는 “어떤 column을 정렬할지”를 위해 sortKey 라는 프롭을 사용하고, “어떤 기준/방향으로 정렬할지”를 정하기 위한 order 프롭을 사용해야했다. 나는 언뜻 이름만 보고 그 역할을 구분하기 쉽지 않은 것 같다고 판단했고, material-ui에서는 이와 비슷한 역할을 하는 프롭이 있나 찾아보니, TableCell 에서는 이미 sortDierction 이라는 같은 역할을 하는 프롭이 존재했다.

tableSortFilterLabel Code r

그래서 우리는 일관성있으면서 “direction”이라는 명확한 의미를 가지도록 ordersortDirection 으로 변경하였다. 이외에도 다양한 경우 최대한 기존 mui 의 디자인 시스템에서 사용되는 용어를 사용하며, 이와 비슷한 컨셉으로 사용하려고 노력중이다.

시스템 용어 정의

여러 디자인 시스템에서는 조금씩 다른 용어로 위계나, 역할, 혹은 사용처를 표현한다.

어떻게 하면 확장 가능하며 사용자가 러닝 커브 없이 원하는 스펙을 사용할 수 있을지 고민하여 네이밍하는 것도 중요하다. 이러한 네이밍은 곧 디자인 시스템의을 사용하는 모든 이의 의사소통 언어가 되기 때문에 더욱 중요하다.

이와 관련해서는 여기서 정리해보았다.

디자인 시스템을 만들며 중요하게 생각한 점 (opens in a new tab)

마치며…

좋은 네이밍을 고민한 시간은 결코 헛되지 않다고 생각한다. 어떤게 “왜” 좋은 이름일지 고민하다보면 자신만의 기준이 생기게 되고, 더 쉽고, 빠르게 가독성있는 코드를 작성하고 컴포넌트를 만들 수 있으며, 궁극적으로는 코드를 읽는 사람(그게 바로 먼 미래의 나일 가능성도 높다)의 비용을 줄일 수 있다.

사실 이렇게 디자인 시스템 설계 가이드라인 관련하여 컴포넌트를 만들 때 흔히 놓치기 쉬운 ref 프롭 노출이나 native HTML attribute에 대한이야기도 하려고 했으나, 이와 관련해서는 여기 카카오 기술블로그 ‘더 가치 있는 공통 컴포넌트 만들기’에 이미 잘 정리가 되어있어서 여기서 마무리 하려고 한다!

더 가치 있는 공통 컴포넌트 만들기 | 카카오엔터테인먼트 FE 기술블로그 (opens in a new tab)