관리자 패널은 설정 시스템이 아니다
출처: Dev.to
이것을 짜증나는 방식으로 배웠어: 관리 패널은 설정 시스템이 아니다.
그들은 될 수 있지만, 우연으로가 아니라 의도적으로이다.
처음엔 무해하게 느껴진다. 누군가는 헤드라인 교체가 필요하고, 한 고객을 위해 섹션을 숨기고 싶어한다. 판매는 하나의 계정에 다른 값을 원한다. 지원팀은 엔지니어링에 기다리지 않고 문제를 해결해야 한다.
그래서 필드가 추가돼.
그다음엔 또 하나.
Then another one.
어느 시점부터 관리 패널은 ‘설정’이 일어나는 장소가 되지만, 실제로는 전혀 제어가 이루어지지 않는다. 그것은 단순히 제품이 읽는 형태로 폼을 작성하는 것에 불과하다.
그건 사람들이 원래 기본값을 잊기 시작할 때까지는 괜찮다.
그럼 아무도 값이 전역인지 고객별인지, 저장된 초안이 실제 적용되는지, 생성된 레코드가 이전값인지 최신값인지, 혹은 되돌리면 한 계정만 영향을 받는지 전체에 영향을 받는지 알 수 없게 된다.
그 시점이 관리 패널이 유용함을 잃고 위험해지는 순간이다.
기본 워크플로우는人们 보통 인정하지 않는 것보다 더 큰 무게를 지닌다. 지원팀은 이를 알고 있다. 문서에서도 이를 가리킨다. 청구 로직은 그에 기반해 구축되었다. 제품의 다른 부분들은 내일도 그 상태가 유지될 것이라고 가정한다.
그래서 나는 더 이상 기본값을 임의 설정처럼 다루지 않는다.
원본은 잠그인 상태여야 한다. 고객이 다른 것이 필요하면 오버라이드를 만든다.それを 복제한다. 버전 관리한다. 미리보기한다. 준비될 때 활성화한다. 하지만 고객 수준의 수정이 원본 템플릿을 변경하도록 해서는 안 된다.
대략적인 형태는 다음과 같다:
const sourceTemplate = {
id: "default_workflow",
locked: true,
sourceOfTruth: true,
sections: [
{ key: "intro", title: "Welcome", visible: true },
{ key: "setup", title: "Setup", visible: true },
{ key: "review", title: "Review", visible: true }
]
};
const customerConfig = {
customerId: "customer_123",
sourceTemplateId: "default_workflow",
status: "draft",
overrides: {
sections: {
setup: {
title: "Account Setup",
visible: true
},
review: {
visible: false
}
}
}
};
원본은 그대로 남아 있다. 고객은 다른 버전을 갖게 된다. 관리 패널은 복제본이나 오버라이드를 편집한다. 원본은 건드리지 않는다.
상태는 많은 제품이 허aph를 일으키는 부분이다.
생산이 ‘마지막에 저장된 값’만 읽게 하고 싶지 않다. 저장과 실제 적용은 동일하지 않다. 초안은 저장될 수 있다. 미리보는 것은 렌더링될 수 있다. 보관된 버전은 역사용으로 존재할 수도 있다. 그 어떤 것도 생산을 제어해서는 안 된다.
보통 나는 다음과 같은 구조를 원한다:
const CONFIG_STATUS = {
DRAFT: "draft",
PREVIEW: "preview",
ACTIVE: "active",
ARCHIVED: "archived"
};
생산은 ‘active’를 읽는다. 최신값이 아니라, 미리보기값도 아니고, 복사 테스트 중 5분 전에 저장한 것이 아니다.
대안を見た 이후에는 엄격해 보이지만, 실제 대안을 보면 그렇지 않다.
대안은 지원팀원이 초안을 저장해 고객이 보는 것을 실수로 바꾸는 경우, 판매 오버라이드가 기존 객체를 편집하는 것이 간단해서 고객별 레이어를 만들기보다는 새로운 기본값이 되는 경우, 혹은 생성된 문서가 지난 주에 승인된 값 supposed을 반영해야 함에도 오늘의 값을 끌어오는 경우와 같다.
그 버그는 언제나 드라마틱하지 않다. 대부분은 단순히 혼잡할 뿐이다. 혼잡한 버그는 코드 상의 실패 라인이 하나도 없다는 점에서 시간이 많이 소모된다. 제품은 명령받은 대로 정확히 동작한다. 문제는 누가 무엇을 하라는지 설명할 수 없다는 것이다.
그래서 나는 설정을 한 곳에 집중하는 것을 선호한다.
모든 컴포넌트가 어떤 버전을 써야 할지 스스로 판단하게 하면 안 된다. 계정 확인, 복사 오버라이드, 기능 플래그, 가격 규칙, 가시성 로직 등을 앱 곳곳에 흩어지게 하면 제품이 쓰레기통이 된다.
앱에 효과적인 설정을 제공하고 렌더링하게 하자.
function resolveEffectiveConfig( customer ) {
const source = getLockedSourceTemplate();
const activeConfig = getActiveCustomerConfig(customer.id);
if (!activeConfig) {
return source;
}
return mergeSourceWithOverrides(source, activeConfig);
}
해당 로직은 심플하다. 활성 고객 설정을 확인한다. 실시간 경로에서 초안과 미리보기를 무시한다. 활성 설정이 없을 경우 잠긴 원본으로 되돌아간다. 허용된 오버라이드만 병합한다.
그 외 모든 것이 결과를 신뢰할 수 있어야 한다.
테스트도 똑같이 심플해야 한다.
초안이 유출되지 않음을 입증하는 테스트를 원한다. 미리보기가 유출되지 않음을 입증하는 테스트도 원한다. 보관된 설정이 갑자기 실제 적용되는 경우가 없다는 것을 입증하는 테스트도 원한다.
expect(resolveLiveConfig(customerWithDraftOnly)).toEqual(sourceDefault);
expect(resolveLiveConfig(customerWithPreviewOnly)).toEqual(sourceDefault);
expect(resolveLiveConfig(customerWithArchivedOnly)).toEqual(sourceDefault);
expect(resolveLiveConfig(customerWithActiveConfig)).toEqual(expectedOverride);
그 테스트는 인상적이지 않게 보이지만, 나쁜 출시로부터 당신을 구해줄 수 있다.
생성 레코드도 동일한 원칙을 따라야 한다.
설정에서 생성된 것이라면, 그 기록이 역사적으로 정확하게 유지되어야 하려면 값을 스냅샷한다. 이후에 live config를 계속 참고해 같은 의미로 해석될 것이라고 가정하면 안 된다.
인보이스, 계약서, 영수증, 감사 로그, 승인, 고객 확인과 같은 기록은 생성 시점의 값을 보존해야 한다.
const generatedSnapshot = {
customerId: "customer_123",
generatedAt: "2026-06-14T00:00:00Z",
configVersion: "v3",
values: {
setupFee: 12000,
monthlyMinimum: 2500,
transactionSharePercent: 20
}
};
현재 설정이 내일 바뀔 수 있다. 어제의 기록은 그대로여야 한다.
그건 좋은 답변이 아니다.
브라우저에 전송되는 내용에 대해서도 훨씬 더 엄격해졌다. 프론트엔드는 전체 내부 설정 객체가 필요하지 않다. 페이지를 렌더링하는 데 필요한 작은 부분만 필요하다.
그래서 프로젝션한다.
function projectPublicConfig(effectiveConfig) {
return {
sections: effectiveConfig.sections
.filter(section => section.visible)
.map(section => ({
key: section.key,
title: section.title,
body: section.body,
cta: section.cta
}))
};
}
브라우저는 렌더링 데이터만을 받는다. 그것만 필요했다.
신뢰하는 버전은 다음과 같다:
const effectiveConfig = resolveEffectiveConfig(customer);
const publicConfig = projectPublicConfig(effectiveConfig);
return publicConfig;
생성 레코드에 대해서는:
const effectiveConfig = resolveEffectiveConfig(customer);
const snapshot = createImmutableSnapshot(effectiveConfig);
saveGeneratedRecord({
customerId: customer.id,
snapshot,
generatedAt: new Date().toISOString()
});
생산 UI에 대해서는:
const config = resolveLiveConfig(customer);
renderWorkflow(config);
예시용으로:
const previewConfig = resolvePreviewConfig(customer, draftConfigId);
renderPreview(previewConfig);
실시간과 미리보기가 동일한 느슨한 경로를 공유하는 것을我不喜欢한다. 그것이 예시 작업이 우연히 실제 동작으로 전이되는 원인이다.
이 모든 것은 설정을 멋지게 만들기 위한 것이 아니다. 거대한 관리 시스템을 자체 목적으로 만들고자 하는 것이 아니다. 제어는 핵심 목표다.
고객은 다양한 요구를 갖게 된다. 내부 팀은 빠르게 움직일 필요가 있다. 제품은 언젠가 계정 수준 행동을 필요로 할 것이다. 그것이 정상적인 일이다.
문제는 모든 이러한 요구가 단순히 d를 만드는 방식으로 해결될 때 시작된다.