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

슬라이더를 포기한 값

~13 min · migration, trade-offs, product, judgment

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

선택: 이음매를 기를까, 손실을 받아들일까

공용 패널은 자기 기하 정보를 앱이 대주는 키 접두사 아래 브라우저 로컬 저장소에 담아. 모드, 너비, 높이. 변환하던 앱 하나는 그 값들을 자기 설정 저장소에 두고 있었고, 게다가 내보내고 있었어. 모드 고르는 것과 크기 슬라이더 둘이 설정 팝오버에 살아 있었지.

선택지 둘. 앱이 자기 저장 백엔드를 대줄 수 있게 공용 컴포넌트에 저장 이음매를 기르거나, 공용 저장을 받아들이고 컨트롤을 지우거나.

결정은 손실을 받아들이는 거였어. 설정 컨트롤 셋이 지워졌고, 뒤에 있던 상태 필드랑 타입도 같이. 근거는 이랬어. 다른 앱들은 그 슬라이더 없이 살고, 가장자리 끌기 크기 조절이 이미 그 일을 해. 그러니까 그 컨트롤은 공용 컴포넌트가 못 푸는 문제가 아니라 다르게 푸는 문제를 풀고 있었던 거야.

이음매를 거절한 게 더 싼 답이었던 이유

저장 이음매는 무해하게 들려. 실제론 계약의 영구적 확장이야. 이제 소비자마다 저장 전략을 고르거나 기본값을 받아야 하고, 공용 컴포넌트는 유지하고 테스트할 코드 경로가 둘이 되고, 저장을 건드리는 다음 기능은 둘 다에서 돌아야 해. 그 값은 영원히, 모두가, 앱 하나가 컨트롤 셋을 지키는 대가로 치러.

그리고 이 결정이 나중에 맞아 보이게 만든 대목이 여기야. 똑같은 질문이 뒤따른 변환 셋 전부에서 나왔고, 어디서도 값이 0 이었어. 그 앱들도 기하 정보를 자기 상태에 두고 있었어. 그런데 아무도 사용자한테 내보내진 않았지. 공용 저장을 받아들이는 게 걔들한텐 안 보였어.

첫 앱을 위해 그 이음매를 붙였으면, 소비자 넷이 하나를 섬기려고 그걸 지고 다녔을 거고, 뒤의 변환 셋이 조용히 그게 불필요하다는 걸 증명했을 거야. 이미 하중을 받게 된 다음에.

제안된 이음매는 요청하는 하나 말고 소비자 전부에 대고 값을 매겨. 눈앞의 소비자는 구체적인 필요랑 이름을 갖고 있고, 그 이음매를 지고 다닐 미래 소비자들은 추상적이고 조용해. 그 비대칭이 모든 개별 이음매를 값어치 있어 보이게 만들어. 확인법은 다음 변환 셋이 그걸로 뭘 할지 묻는 거야. 정직한 답이 "아무것도"면, 그 이음매는 앱 하나를 위해 모두 부담으로 붙는 거야.

저장하는 게 아니라 내보낸 걸 물어

값이 정확히 한 자리에서만 진짜였게 만든 그 구분을 뽑아둘 값이 있어. 앱 넷 다 패널 기하 정보를 저장했어. 그걸 위한 인터페이스를 지은 건 하나뿐이었고. 내부 상태는 똑같았고, 사용자한테 보이는 약속은 안 똑같았지.

변환에 값을 매길 때 질문은 앱이 뭘 쥐고 있냐가 아니라 뭘 약속했냐야. 내부 상태는 조용히 옮길 수 있어. 누가 써본 컨트롤은 못 옮기고, 진짜 값은 거기 살아. 그러니까 값 매기는 질문은 "이 앱이 뭘 저장해"가 아니라 "이 앱이 내보낸 것 중에 사용자가 사라진 걸 알아챌 수 있는 게 뭐야"야.

Code

거절된 이음매, 그리고 거절이 치른 값·typescript
// THE SEAM THAT WAS PROPOSED - reasonable-looking, and permanent.
// Every consumer would now choose or default a storage strategy,
// and the shared component would carry two paths forever.
type PanelProps = {
  storage?: {                        // <- REFUSED
    get(key: string): string | null;
    set(key: string, value: string): void;
  };
};

// WHAT SHIPPED INSTEAD: one storage strategy, app-supplied prefix.
function usePanelGeometry(prefix: string) {
  const key = `${prefix}.panel`;
  const [geometry, setGeometry] = useState(() =>
    JSON.parse(localStorage.getItem(key) ?? "null") ?? DEFAULTS
  );
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(geometry));
  }, [key, geometry]);
  return [geometry, setGeometry] as const;
}


// WHAT THE CONVERTING APP DELETED - the actual invoice line:
//
//   - <ModePicker  value={pippaPanelMode}  onChange={...} />
//   - <SizeSlider  label="Width"  value={pippaPanelWidthPx}  ... />
//   - <SizeSlider  label="Height" value={pippaPanelHeightPx} ... />
//   - pippaPanelMode / WidthPx / HeightPx  from the settings store
//   - the three setters and their types
//
// The next three conversions asked the SAME question and deleted
// NOTHING, because none of them had ever rendered these controls.
// Their geometry was internal state, and internal state moves
// silently.


// THE PRICING QUESTION, which is not the one people ask:
//
//   not  "what does this app STORE?"        (all four: geometry)
//   but  "what has this app SHIPPED that a  (one of four: sliders)
//         user could notice disappearing?"

External links

Exercise

네가 관리하는 공용 컴포넌트에서 소비자 하나가 요청해서 생긴 설정 옵션을 하나 찾아. 실제로 기본값이 아닌 값을 넘기는 소비자가 몇인지 확인해. 답이 하나나 0 이면, 그걸 없앨 때 그 소비자가 치를 값이랑 그걸 지고 다니는 게 나머지 모두한테 코드 경로랑 테스트 케이스로 얼마인지 적어.
Hint
코드 줄 말고 테스트 케이스를 세. 의미 있는 값이 둘인 옵션은 그 아래 모든 것의 상태 공간을 두 배로 만들고, 진짜 지속 비용은 분기 자체가 아니라 그 곱셈에 살아.

Progress

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

댓글 0

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

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