본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

슬롯, prop, 그리고 예상 못 한 것

~12 min · ui, extension-points, components, design

Level 0흩어진 부품
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

앱이 달라도 되게 하는 방법 둘

조수 표면을 앱에 붙이는 그 공용 UI 껍데기는 시각 정체성이 다르고, 자기 컨트롤을 다르게 설명하고, 플랫폼도 다른 호스트 열몇을 받아내야 해. 확장 지점 딱 두 종류로 그걸 해. 그 구분을 쥐고 있을 값이 있어.

  • prop 은 넘기는 값이야. 임베드 URL, 저장소 키 접두사, 헤더 문구, 상징 기호. 공용 컴포넌트가 뭘 받을지 모양도 알고 어디 놓을지도 알아.
  • 슬롯 은 앱이 그리는 뭐든 채우는 구역이야. 공용 컴포넌트는 어디 놓을지만 알고, 그게 뭔지는 아무것도 몰라.

prop 이 따지기 싸고 기본이어야 해. 슬롯은 모양을 미리 아는 것 자체가 실수인 경우를 위한 거고.

슬롯을 증명한 사례

호스트마다 패널의 사용 불가 상태에 문장 하나를 넣어. 조수가 오프라인이야, 잠시 뒤에 다시 해봐. 호스트 하나가 달라. 로컬 모델을 싣고 다니는 데스크톱 앱이라, 원격 조수가 안 닿아도 여전히 대화를 이어갈 수 있거든. 남들이 한 줄 넣는 자리에 대화형 채팅 표면을 통째로 넣고 싶어 했어.

공용 컴포넌트는 아무것도 안 바뀌어도 됐어. 슬롯이 이미 임의 내용을 받고 있었고, 앱이 자기 오프라인 표면을 대주고 자기 스타일시트에서 슬롯 기본 배치의 가운데 정렬을 풀었어. prop 이었으면 이걸 미리 알아야 했을 거야. offlineMessage 문자열은 채팅 인터페이스를 담을 수 없고, 담을 만큼 일반화했을 즈음이면 단계만 더 붙은 슬롯을 발명한 거지.

확장 지점이 아무도 설계 안 한 용도를 삼키면, 그게 맞는 종류라는 증거야. 이음매의 시험은 네가 열거한 경우들을 처리하느냐가 아냐. 그것들로 지었으니까 당연히 처리하지. 네가 안 열거한 하나를 처리하느냐야. 새 용도마다 변경이 필요한 이음매는 추상의 옷을 입은 축복받은 변형 목록이야.

이음매 하나로 부족할 때

두 번째 슬롯 모양이 나중에 붙었는데, 이유가 배울 만해. 설계가 아니라 DOM 얘기거든. 앱들은 자기 컨트롤을 두 가지로 설명해. 어떤 앱은 컨트롤에 속성을 얹어. 제목, aria 설명, 추가 핸들러. 어떤 앱은 컨트롤 옆에 형제 노드를 만들어내는 툴팁 컴포넌트를 그려. 그건 속성을 아무리 많이 허용해도 그 컨트롤의 속성으로는 표현이 안 돼.

그래서 이음매가 둘이야. 컨트롤에 속성 병합하기, 아니면 컨트롤을 앱 자기 노드로 감싸기. 둘 다 선택이고, 둘 다 안 쓰는 소비자를 안 건드려. 배울 건 둘을 내보내라가 아냐. 두 번째가 첫 번째로는 표현이 안 된다는 진짜 사례가 증명했을 때 붙었다는 거야. 둘이 더 유연하게 들려서 미리 붙인 게 아니라.

Code

prop, 슬롯, 그리고 앱이 컨트롤을 입히는 두 가지·typescript
type SidekickProps = {
  // PROPS: values. The component knows the shape and the destination.
  embedUrl: string;
  storagePrefix: string;
  title?: string;
  mark?: string;              // an app emblem for the header
  iframeAllow?: string;       // one app needs microphone; see track 7

  // SLOTS: regions. The component knows only WHERE, never WHAT.
  beforeFrame?: React.ReactNode;
  unavailable?: React.ReactNode;

  // SEAMS for dressing a control the component owns. Two shapes,
  // because apps explain themselves two different ways:
  controlProps?: (id: string) => Record<string, unknown>;   // attributes
  controlWrap?: (id: string, node: React.ReactNode) => React.ReactNode;
};

function Control({ id, children, controlProps, controlWrap }: ControlArgs) {
  const node = <button {...(controlProps?.(id) ?? {})}>{children}</button>;
  // A tooltip renders a SIBLING node beside the button, which no
  // number of attributes on the button could ever express. That is
  // why the second seam exists - and it was added when a real case
  // proved the first could not do it, not pre-emptively.
  return controlWrap ? controlWrap(id, node) : node;
}

// Almost every app passes a sentence to `unavailable`:
//
//   unavailable={<p>Assistant is offline. Try again shortly.</p>}
//
// One passes an entire local-model chat surface:
//
//   unavailable={<OfflineChat model="local" persona={persona} />}
//
// The shared component did not change for that. The app un-centered
// the slot in its own stylesheet:
//
//   .app .sidekick-unavailable { display: block; text-align: left; }

External links

Exercise

네가 소유한 공용 컴포넌트를 잡고 확장 지점을 나열해. 각각을 값인지 구역인지 표시하고. 그다음 컴포넌트 자체를 고쳐야 했던 제일 최근 기능 요청을 찾아. 기존 확장 지점이 그걸 삼킬 수 있었을지 물어. 아니었으면 맞는 처방이 새 prop 이었을지 슬롯이었을지도.
Hint
"슬롯이었어야 했다"의 증거는 타입이 시간을 두고 넓어진 prop 이야. 문자열이었다가 문자열-또는-노드가 되고, 그다음 선택적 렌더 함수가 붙은 것. 넓힘 하나하나가 값한테 구역 일을 시키려던 시도였어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.