명시성: 무엇을 암시하고 무엇을 명시할 것인가
어떤 규칙은 코드에 숨긴 채로 두어도 될까,
어떤 규칙은 코드가 직접 드러내야 할까?
들어가며 — 암시는 언제 비용이 되는가
코드를 작성할 때
자연스럽게 이런 선택을 한다.
// 암시적 설계
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// 사용
if (isLoading) return <Loading />;
if (isError) return <Error />;
if (isSuccess) return <Success />;
이 코드는 작동한다.
하지만 여기에는 암시된 규칙이 있다.
“이 세 상태는 동시에 true가 될 수 없다.”
이 규칙은 코드 어디에도 명시되어 있지 않다.
타입 시스템도 이를 막지 못한다.
// 이게 가능하다 (하지만 말이 안 됨)
setIsLoading(true);
setIsSuccess(true); // 동시에?
개발자는 이 규칙을 기억해야 한다.
하나라도 잊으면 버그가 된다.
이 문서는
암시적 설계가 왜 문제가 되는지,
그리고 명시적 설계가 무엇을 해결하는지를
다시 생각해보기 위해 쓰였다.
명시성과 암시성
명시적 설계
명시적 설계는
코드 자체가 모든 규칙을 드러내는 설계다.
// 명시적 설계
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
// 사용
if (status === 'loading') return <Loading />;
if (status === 'error') return <Error />;
if (status === 'success') return <Success />;
이제 불가능한 상태는
타입 시스템이 막아준다.
loading과success가 동시에 true? → 불가능- 세 개의 상태를 동기화할 책임? → 사라짐
- 규칙을 기억해야 하는 부담? → 사라짐
암시적 설계
암시적 설계는
코드에 드러나지 않은 규칙을 가정하는 설계다.
// 암시적 설계
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// 암시된 규칙:
// - 이 세 상태는 동시에 true가 될 수 없다
// - 하나가 true가 되면 나머지는 false여야 한다
// - 하지만 이 규칙은 코드에 없다
이 규칙은:
- 코드에 없다
- 타입 시스템이 보호하지 않는다
- 개발자가 기억해야 한다
- 문서에 의존한다
암시적 설계의 비용
1. 기억해야 하는 부담
암시적 설계는
개발자가 규칙을 기억해야 한다고 가정한다.
// 암시적: 매번 세 개를 올바르게 설정해야 함
const handleSubmit = async () => {
setIsLoading(true);
setIsError(false); // 기억해야 함
setIsSuccess(false); // 기억해야 함
try {
await submit();
setIsLoading(false);
setIsError(false); // 기억해야 함
setIsSuccess(true);
} catch (error) {
setIsLoading(false);
setIsError(true);
setIsSuccess(false); // 기억해야 함
}
};
하나라도 잊으면 버그가 된다.
2. 불가능한 상태를 허용한다
암시적 설계는
논리적으로 불가능한 상태를 만들 수 있다.
// 이게 가능하다 (하지만 말이 안 됨)
setIsLoading(true);
setIsSuccess(true); // 동시에 로딩 중이고 성공?
// 이것도 가능하다 (하지만 말이 안 됨)
setIsError(true);
setIsSuccess(true); // 동시에 에러이고 성공?
// 이것도 가능하다 (하지만 말이 안 됨)
setIsLoading(false);
setIsError(false);
setIsSuccess(false); // 아무 상태도 아닌 상태?
이런 상태들이 실제로 발생하면
버그를 찾기 어려워진다.
3. 동기화의 책임
암시적 설계는
여러 상태를 동기화하는 책임을 개발자에게 전가한다.
// 상태 간 의존성이 암시적
const [isEditing, setIsEditing] = useState(false);
const [editedValue, setEditedValue] = useState('');
// 규칙:
// - isEditing이 true일 때만 editedValue가 의미 있음
// - isEditing이 false가 되면 editedValue는?
// - 이 규칙은 코드에 없다
이런 의존성이 많아질수록
동기화의 책임도 커진다.
4. 이해의 비용
암시적 설계는
코드를 읽는 사람이 규칙을 추론해야 한다.
// 이 코드를 읽는 사람은:
const [count, setCount] = useState(0);
const [isEven, setIsEven] = useState(true);
useEffect(() => {
setIsEven(count % 2 === 0);
}, [count]);
// 다음을 추론해야 함:
// - isEven은 count로부터 계산 가능
// - count가 바뀌면 isEven도 바뀜
// - 하지만 이 관계는 코드에 명시되지 않음
코드를 읽는 사람은
암시된 규칙을 발견해야 한다.
명시적 설계의 가치
1. 타입 시스템이 보호한다
명시적 설계는
타입 시스템이 불가능한 상태를 막아준다.
// 명시적: 타입이 불가능한 상태를 막음
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
// 이건 컴파일 에러
setStatus('loading');
setStatus('success'); // 이미 'loading'인데?
타입 시스템이
개발자가 실수할 수 있는 경우를 줄여준다.
2. 규칙을 기억할 필요가 없다
명시적 설계는
코드 자체가 규칙을 드러낸다.
// 명시적: 코드가 규칙을 드러냄
type EditMode =
| { type: 'viewing' }
| { type: 'editing'; value: string };
const [mode, setMode] = useState<EditMode>({ type: 'viewing' });
// 규칙이 코드에 있다:
// - viewing 모드에는 value가 없음
// - editing 모드에는 value가 필수
// - 기억할 필요 없음
코드를 읽으면
규칙이 바로 보인다.
3. 동기화 책임이 사라진다
명시적 설계는
상태 간 의존성을 구조로 표현한다.
// 명시적: 의존성이 구조에 있음
type CountState = {
count: number;
isEven: boolean; // count로부터 계산 가능
};
// 또는 더 명시적으로
const [count, setCount] = useState(0);
const isEven = count % 2 === 0; // 파생 값, 상태 아님
의존성이 구조에 있으면
동기화할 필요가 없다.
4. 이해가 쉬워진다
명시적 설계는
코드를 읽는 사람이 추론할 필요가 없다.
// 명시적: 추론 불필요
type UserState =
| { type: 'loading' }
| { type: 'success'; user: User }
| { type: 'error'; error: Error };
const [state, setState] = useState<UserState>({ type: 'loading' });
// 가능한 상태가 명확함:
// - loading: 데이터 로딩 중
// - success: 사용자 데이터 있음
// - error: 에러 발생
// 추론할 필요 없음
코드를 읽으면
가능한 상태가 바로 보인다.
암시적 설계가 생기는 이유
1. 편의성
암시적 설계는
처음에는 편리해 보인다.
// 간단해 보임
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
여러 상태를 따로 관리하는 것이
처음에는 단순해 보인다.
하지만 시간이 지나면서
상태 간 의존성이 생기고,
규칙이 복잡해지고,
동기화 책임이 커진다.
2. 점진적 복잡도
암시적 설계는
점진적으로 복잡해진다.
// 처음: 단순
const [isLoading, setIsLoading] = useState(false);
// 나중: 에러 추가
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// 더 나중: 성공 추가
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// 어느 순간: 규칙이 복잡해짐
// 하지만 여전히 암시적
각 단계에서는
여전히 관리 가능해 보인다.
하지만 어느 순간부터
규칙을 기억하기 어려워진다.
3. 타입 시스템의 한계
일부 언어나 프레임워크는
명시적 설계를 표현하기 어렵다.
// JavaScript에서는 타입이 없어서
// 명시적 설계를 강제하기 어려움
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// TypeScript를 쓰면 가능
type Status = 'loading' | 'error' | 'success';
const [status, setStatus] = useState<Status>('loading');
하지만 TypeScript를 쓴다고 해서
자동으로 명시적 설계가 되는 것은 아니다.
의도적으로 명시적 설계를 선택해야 한다.
명시적 설계의 비용
명시적 설계도 비용이 있다.
1. 초기 작성 비용
명시적 설계는
처음 작성할 때 더 많은 코드가 필요하다.
// 암시적: 간단
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// 명시적: 더 많은 코드
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
타입을 정의하고,
구조를 설계하는 데 시간이 걸린다.
2. 변경 비용
명시적 설계는
변경할 때 타입도 함께 수정해야 한다.
// 새 상태 추가 시
type Status =
| 'idle'
| 'loading'
| 'success'
| 'error'
| 'retrying'; // 타입 수정 필요
// 모든 사용처 확인 필요
하지만 이 비용은
명시적 설계가 주는 이득보다 작다.
3. 학습 곡선
명시적 설계는
패턴을 학습해야 한다.
// 명시적 설계 패턴
type State =
| { type: 'A'; value: string }
| { type: 'B'; count: number };
// 이 패턴을 이해해야 함
하지만 한 번 이해하면
다른 곳에서도 적용할 수 있다.
명시성의 판단 기준
모든 것을 명시적으로 만들 필요는 없다.
명시적으로 만들 가치가 있는 것
1. 상태 간 의존성
// ❌ 암시적: 의존성이 숨어 있음
const [isEditing, setIsEditing] = useState(false);
const [editedValue, setEditedValue] = useState('');
// ✅ 명시적: 의존성이 구조에 있음
type EditMode =
| { type: 'viewing' }
| { type: 'editing'; value: string };
2. 상호 배타적인 상태
// ❌ 암시적: 동시에 true 가능
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// ✅ 명시적: 동시에 불가능
type Status = 'loading' | 'success' | 'error';
3. 파생 가능한 값
// ❌ 암시적: 파생 상태
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
useEffect(() => {
setCount(items.length);
}, [items]);
// ✅ 명시적: 파생 값
const [items, setItems] = useState([]);
const count = items.length;
암시적으로 두는 선택도 가능한 것
1. 독립적인 상태
// 독립적이면 암시적이어도 괜찮음
const [username, setUsername] = useState('');
const [theme, setTheme] = useState('light');
// 서로 무관하므로 명시적 설계 불필요
2. 단순한 계산
// 단순한 계산은 암시적이어도 괜찮음
const isEmpty = items.length === 0;
const isValid = input.length >= 3;
// 복잡하지 않으므로 명시적 설계 불필요
명시성의 단계
명시성은 단계적이다.
1단계: 암시적
// 모든 규칙이 암시적
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
2단계: 주석으로 명시
// 주석으로 규칙 명시 (하지만 타입 시스템이 보호하지 않음)
// 이 세 상태는 동시에 true가 될 수 없음
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
3단계: 타입으로 명시
// 타입으로 명시 (타입 시스템이 보호함)
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
4단계: 구조로 명시
// 구조로 명시 (의존성까지 명시)
type State =
| { type: 'idle' }
| { type: 'loading' }
| { type: 'success'; data: Data }
| { type: 'error'; error: Error };
const [state, setState] = useState<State>({ type: 'idle' });
정리하며 — 명시성은 선택이다
명시적 설계는
항상 더 나은 것은 아니다.
하지만 의존성과 규칙이 있는 곳에서는
규칙을 구조로 옮겼을 때 비용이 줄어드는 사례가 반복된다.
암시적 설계의 비용
- 기억해야 하는 부담
- 불가능한 상태를 허용
- 동기화의 책임
- 이해의 비용
명시적 설계의 가치
- 타입 시스템이 보호
- 규칙을 기억할 필요 없음
- 동기화 책임이 사라짐
- 이해가 쉬워짐
명시적 설계의 비용
- 초기 작성 비용
- 변경 비용
- 학습 곡선
판단 기준
의존성과 규칙이 있는 곳에서는
명시적으로 만드는 선택을 고려하고,
독립적이고 단순한 곳에서는
암시적으로 두는 선택도 가능하다.
명시성은
규칙을 기억하지 않아도 되게 만드는 선택이다.
의존성과 제약이 있다면,
그 규칙을 구조로 옮기는 편이 더 설명 가능해질 때가 많다.
명시적 설계는
코드를 읽는 사람이
추론하지 않아도 되게 만든다.
이것이 명시성의 가치다.