500줄의 C 코드로 되살린 VisiCalc, 47년간 변하지 않은 스프레드시트의 본질
TL;DR
1979년 Apple II를 킬러앱으로 만든 VisiCalc을 500줄의 C 코드로 재구현한 프로젝트가 스프레드시트의 본질을 보여준다. 셀, 수식, 참조, 재계산이라는 핵심 추상화는 47년간 변하지 않았지만, 원본이 16KB RAM에서 동작한 반면 현대적 구현은 171KB를 사용한다. 전체 그리드 재계산 방식은 단순하지만, 실무급 스프레드시트에는 의존성 그래프 기반 증분 재계산이 필수적이다. 이 프로젝트는 데이터 모델링, 파싱, 의존성 관리, 반응형 UI가 결합된 프로그래밍의 축소판을 보여준다.
Key Takeaways
- 스프레드시트의 데이터 모델은 극도로 단순하다: 각 셀은 EMPTY, NUM, LABEL, FORMULA 네 가지 타입 중 하나이며, 이 추상화는 1979년이나 2026년이나 동일하다
- 재귀 하강 파서(recursive descent parser)로 수식을 평가하면 연산자 우선순위가 호출 깊이로 자연스럽게 해결된다: 에러 처리는 IEEE 754의 NAN 전파 특성을 활용해 우아하게 구현 가능
- 전체 그리드 재계산은 구현은 쉽지만 실무에는 부적합하다: 수천 개 이상의 셀이 얽힌 실무 스프레드시트에서는 의존성 그래프 기반 증분 재계산이 필수적이며, 이는 빌드 시스템과 유사한 문제다
- 메모리 제약이 설계를 결정한다: 원본 VisiCalc은 16KB RAM에서 동작하기 위해 빈 셀에 메모리를 할당하지 않았지만, 현대적 정적 배열 구현은 171KB를 소비한다
- 표면적 단순함 아래 깊은 복잡성이 숨어있다: 파일 I/O, 상대/절대 참조, 순환 참조 감지, undo 같은 기능들이 실제 프로덕션 스프레드시트와 교육용 프로토타입을 구분한다
상세 내용
셀이라는 보편적 추상화
스프레드시트의 데이터 모델은 놀랄 만큼 단순하다. zserge의 구현에서 셀 하나는 타입 플래그, float 값, 그리고 사용자 입력 원문을 담는 128바이트 버퍼로 구성된다:
struct cell {
int type; // EMPTY, NUM, LABEL, FORMULA
float val;
char text[MAXIN];
};
그리드는 26열(A~Z) × 50행의 2차원 배열이다. 원본 VisiCalc은 256행 × 64열, 현대의 Excel은 104만 행 × 1만 6천 열을 지원하지만, 핵심 추상화는 동일하다.
이 구현의 정적 메모리 할당량은 약 171KB인데, 실제 Apple II의 16KB RAM에서는 절대 동작할 수 없는 크기다. 원본 VisiCalc이 어셈블리 수준에서 얼마나 극한의 메모리 최적화를 했는지 역으로 보여주는 대목이다. 빈 셀에는 메모리를 할당하지 않고, 입력된 셀만 동적으로 관리했을 가능성이 높다. 1979년의 메모리 관리는 그 자체로 하나의 엔지니어링 도전이었다.
재귀 하강 파서로 수식 평가하기
VisiCalc에서 수식은 =가 아니라 +로 시작한다. +A1+A2*B1 같은 형태다. 이를 평가하기 위해 고전적인 재귀 하강 파서(recursive descent parser)를 구현했다.
파서 구조를 따라가면 컴파일러 교과서의 축약판을 읽는 느낌이다. 최상위 expr 함수가 덧셈·뺄셈으로 분리된 term을 처리하고, term은 곱셈·나눗셈으로 분리된 primary를 처리한다. primary는 숫자, 셀 참조, @SUM 같은 함수 호출, 괄호 표현식 중 하나다. 연산자 우선순위가 파서의 호출 깊이로 자연스럽게 해결되는 구조다.
struct parser {
const char *s, *p;
struct grid *g;
};
셀 참조 파싱도 흥미롭다. 단순히 sscanf로 A1을 읽을 수도 있지만, AB123 같은 다중 문자 열 이름도 지원하도록 직접 파싱한다. 열 문자를 26진법 숫자로 변환하고 행 번호를 정수로 읽는, 작지만 실용적인 설계다.
에러 처리는 NAN 전파로 우아하게 해결했다. NAN에 어떤 연산을 해도 NAN이 되는 IEEE 754 부동소수점의 특성을 활용한 것이다. 별도의 에러 코드나 예외 처리 없이 수식 체인 전체에 에러가 자연스럽게 퍼진다.
다만 원문의 코드 스니펫과 실제 GitHub 소스 사이에 불일치가 있다. 글에서 파서 구조체의 멤버는 pos(정수)로 정의되어 있지만, 본문 코드에서는 *p->p를 반복적으로 참조한다. GitHub의 실제 코드를 보면 멤버 변수가 const char *s, *p로 변경되어 있다. 따라해보려는 독자라면 GitHub 소스를 기준으로 삼는 게 안전하다.
재계산 전략: 단순함의 힘과 한계
스프레드시트의 핵심 난제는 반응성(reactivity)이다. A1이 바뀌면 A1을 참조하는 모든 셀이 연쇄적으로 갱신되어야 한다. 이를 해결하는 방법은 크게 두 가지다: 의존성 그래프를 추적해서 변경된 셀만 정확히 갱신하거나, 전체 그리드를 반복 재계산하거나.
zserge는 원본 VisiCalc의 접근법을 따랐다. 셀이 변경될 때마다 전체 그리드를 행 우선 순서로 순회하며 모든 수식을 재평가한다:
void recalc(struct grid *g) {
for (int pass = 0; pass < 100; pass++) {
int changed = 0;
// 전체 셀을 순회하며 수식 재평가
// 값이 변경되면 changed = 1
if (!changed) break;
}
}
한 번의 패스로 모든 의존성이 해결되지 않을 수 있으므로(가령 A3이 A1을 참조하는데 A1의 수식이 아직 계산되지 않은 경우), 변경이 감지되지 않을 때까지 최대 100번 반복한다.
원본 VisiCalc은 이 재계산이 느려서 수동 재계산 명령(!)을 제공했고, 매뉴얼에서는 "모든 의존성이 해결될 때까지 여러 번 실행하라"고 안내했다. 당시 컴퓨터에서는 대형 스프레드시트의 재계산에 수 초가 걸렸기 때문이다.
zserge는 글에서 "의존성 그래프 유지는 대부분의 스프레드시트에서 과도한 설계(overkill)"라고 썼는데, 이 부분이 Hacker News에서 가장 뜨거운 반론을 불러일으켰다. 복수의 댓글이 "과도하기는커녕, 장난감 수준을 넘어서는 모든 스프레드시트에서 의존성 그래프는 절대적으로 필요하다"고 반박했다.
실제로 현대 Excel의 재계산 엔진은 정교한 의존성 추적 시스템 위에 구축되어 있다. Microsoft Research의 유명한 논문 "Build Systems à la Carte"는 스프레드시트를 일종의 빌드 시스템으로 분석하면서, 의존성 그래프 기반 증분 재계산이 왜 필수적인지를 이론적으로 보여준다. 수천 개의 셀이 복잡하게 얽힌 실무 스프레드시트에서 매번 전체를 재계산하면 사용자 경험이 심각하게 저하된다.
47년간 변하지 않은 것, 그리고 변한 것
이 프로젝트가 증명하는 것은 스프레드시트의 핵심 추상화가 놀랍도록 안정적이라는 사실이다. 셀, 수식, 참조, 재계산, 그리드. 1979년이나 2026년이나 동일하다. VisiCalc을 FORTRAN 66으로 재구현해서 PDP-11에서 돌리는 프로젝트도 있고, VisiCalc의 원저자 Dan Bricklin이 직접 2008년경에 JavaScript로 만든 SocialCalc도 있다. 언어와 플랫폼이 바뀌어도 본질은 그대로다.
그런데 변하지 않은 것 못지않게, 47년 동안 쌓여온 복잡성도 주목할 만하다. 이 구현에서 빠진 것을 나열하면 오히려 현대 스프레드시트의 진짜 어려움이 보인다:
- 파일 I/O
- 수식 복사 시 셀 참조 자동 조정(상대/절대 참조)
- 열 너비 조절
- 행/열 고정
- 범위 이동
- 되돌리기(undo)
- 순환 참조 감지와 복구
한 교육자는 고등학생 AP 컴퓨터과학 수업에서 스프레드시트 구현을 프로젝트로 진행했는데, 일부 학생들의 구현이 "과하게 정교해져서" 보는 재미가 쏠쏠했다고 회상했다. 입력값을 원본 그대로 저장할지 가공해서 저장할지 같은 설계 판단에서 시니어 개발자도 배울 점이 있었다고 한다.
스프레드시트는 프로그래밍의 축소판이다. 데이터 모델링, 파싱, 의존성 관리, 반응형 UI가 모두 들어 있다. 500줄의 C 코드로 이 본질을 꺼내 보여준 것이 이 프로젝트의 진짜 가치다. 그리고 "그게 다가 아니다"라고 지적하는 커뮤니티의 토론이, 표면 아래에 숨은 깊이를 드러내 준다.





