들어가기에 앞서
- 글에 들어가기 앞서 이 글은 ‘프로그래밍은 수학이다’ 라는 생각을 증명하기 위해 작성하는 글이 아니다.
- 프로그래밍은 시대와 맥락에 따라 기술(단순 계산을 돕는 도구), 수학(논리, 증명), 과학(실험, 관찰), 공학(현실의 문제 해결)의 성격을 가지게 되었고 한 단어로 정의할 수 없다.
- 다익스트라의 글을 읽으면서 TypeScript 작성에 대한 생각을 나열한 필자의 생각이다.
프로그래밍은 실험이 아니라 증명이다.
- 알고리즘을 공부하다보면 알 수 밖에 없는 이름이 있다 바로 다익스트라이다.
- 에츠허르 다익스트라(Edsger Wybe Dijkstra)에 대해 GOTO문의 해로움에 대한 문제, 다익스트라 알고리즘을 잘 알고 있지만 ‘프로그래밍을 수학 기반 활동’으로 인식하려고 했던 개발자라는 것은 많은 사람들이 알지 못한다.
Programming is one of the most difficult branches of applied mathematics(프로그래밍은 응용수학의 가장 어려운 분야 중 하나다) - [다익스트라 글 링크]
- 프로그래밍을 기술(skill)이 아니라 수학적 사고가 필요한 학문(applied mathematics)로 바꾼 사람이다.
- 프로그래밍을 '수학과 비슷하다'가 아니라, '증명처럼 구성되어야 하는 활동'이라고 주장했다.
- 다익스트라 이전, 프로그래밍 = 시행착오 + 디버깅
- 다익스트라 이후, 프로그래밍 = 명세 → 수학적 변환 → 프로그램
프론트엔드와의 관계
- 다익스트라의 주장은 어떻게 보면 프론트엔드 분야보다는 백엔드 분야와 가깝다고 생각할 수 있다.
- 실무에서 프론트엔드 개발은 수학보다는 과학에 가깝다는 생각을 하게 된다.
- 실험을 통해 반례를 하나씩 줄여나가는 방식과 실무에서 주로 사용하는 짧은 주기로 버그를 줄여나가는 애자일 방식이 비슷하기 때문이다. (동작 확인 중심)
- 프론트엔드는 사용자 상호작용, 네트워크, 브라우저 환경 등 비결정적인 요소를 많이 포함한다.
- 동일한 입력에도 결과가 달라질 수 있어, 실험과 관찰을 통해 안정성을 확보하는 방식이 자연스럽다.
다익스트라식 프론트엔드
- 필자가 생각하는 다익스트라식 프론트 엔드는 이렇다
- 상태/로직을 수학적으로 정의
- '이 상태에서 이 동작은 반드시 이렇게 된다'를 증명 가능하게 설계
- 테스트를 보조 수단
- 다익스트라는 “테스트는 버그의 존재를 보여줄 뿐, 부재를 증명하지 못한다.”라고 주장했다.
프론트엔드에서의 사용 방식
1. 상태를 값이 아닌 수학적 모델로 본다.
- Before
- 상태에 대한 제한이 없다.
- 상태만 있고 데이터가 없다.
const [status, setStatus] = useState('idle');
- 다익스트라식
- 불가능한 상태가 없다.
- “증명된 상태 구조”를 사용
type State =
| { type: 'idle' }
| { type: 'loading' }
| { type: 'success'; data: Data }
| { type: 'error'; message: string }
2. 조건문이 아니라 “완전한 분기”를 강제한다.
- 모든 케이스를 반드시 처리한다.
_exhaustive는 ‘모든 경우를 다 처리했는지 체크를 위한 관례적인 변수명’
- 빠진 케이스가 있으면 컴파일 단계에서
function render(state: State) {
switch (state.type) {
case 'idle':
case 'loading':
case 'success':
case 'error':
return ...
default:
const _exhaustive: never = state
return _exhaustive
}
}
3. Side Effect를 격리한다.
useEffect(() => {
fetchData().then(setData);
}, [])
function useUser() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
fetchUser()
.then(data => dispatch({ type: 'success', data }))
.catch(err => dispatch({ type: 'error', message: err.message }))
}, [])
}
- Side Effect를 제거하는 것이 아니라, ‘통제 가능한 위치로 옮기는 것’ 이다.
4. 테스트는 증명을 보조할 뿐이다.
- Before
- 테스트 통과 여부를 품질 보장의 지표로 여긴다.
- 다익스트라식
- 테스트는 증명을 대체하는 수단이 아닌, ‘증명을 보완하는 수단’으로 봐야 한다.
- 테스트를 아무리 많이 써도 틀릴 수 있다.
- 증명된 로직을 조합해 테스트 없이도 안전하게 사용할 수 있다.
5. 상태 흐름 자체를 설계한다.
idle
-> loading
-> success | error
프론트엔드에서의 이점
1. 버그가 발생할 공간 자체를 제거
null 체크 지옥이 사라짐
undefined 상태를 제거
2. 리팩토링 안전성 증대
- 타입이 깨지는 순간 컴파일 에러
- 논리 흐름의 깨짐을 런타임까지 끌고 가지 않음
3. 협업 난이도 낮춤
- 코드를 읽는 것이 아니라 논리 흐름을 파악하면 됨
단점
1. 초기 설계 비용 증가
2. 오버 엔지니어링의 가능성 존재
결론
다익스트라의 주장과 논리를 따라서 TypeScript를 설계하는 법을 생각해보고 이점과 단점을 알아봤다.
- 프론트엔드 코드를 ‘동작하는 코드’에서 ‘증명 가능한 시스템’으로 바꾸는 것은 충분한 의미를 갖는 접근이다.
- 결국 중요한 것은 ‘테스트를 얼마나 많이 작성했는가’가 아니라 애초에 ‘틀릴 수 없게 만드는 것’이다.
- 프론트엔드에서도 일부 영역은 실험이 아닌 증명 기반의 수학의 논리로 접근할 수 있다.
- 특히 상태와 로직 설계에서는 다익스트라의 관점이 여전히 유효하다.
모든 프론트엔드를 수학으로 만들자는 주장이라기 보다, 최소한 '복잡한 상태와 로직' 만큼은 증명 가능한 영역으로 끌어올리자는 필자의 제안이다.
이런 시각으로 프론트엔드를 바라본 글 처음 봤어요. 저장해두고 팀원들한테 공유해야겠어요