← SEARCHABLE ARCHIVE
Nand2Tetris · 2026-05-23

Nand2Tetris 한국어 강의자료 — 전체 로드맵

논리 게이트부터 운영체제까지 12개 장을 한 번에 연결하기

INTERACTIVE READING MODE →

Nand2Tetris 한국어 강의자료

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

Nand2Tetris는 NAND 게이트 하나에서 시작해서 컴퓨터, 컴파일러, 운영체제까지 직접 만드는 과정이다. 각 장만 따로 보면 HDL 코드나 Python 코드 모음처럼 보이지만, 사실 앞 장의 결과물이 다음 장의 재료가 된다.

좋다. 이 글에서는 기존 1장부터 12장까지의 코드를 하나의 흐름으로 연결해보자.

전체 흐름

NAND
  ↓
논리 게이트
  ↓
가산기와 ALU
  ↓
레지스터와 메모리
  ↓
Hack CPU와 컴퓨터
  ↓
어셈블러
  ↓
VM 번역기
  ↓
Jack 컴파일러
  ↓
Jack 운영체제

1-5장은 하드웨어를 만들고, 6-8장은 저수준 번역기를 만든다. 9-12장은 고급 언어와 컴파일러, 운영체제를 올린다.

1장. 불 논리와 기본 게이트

목표

  • NAND만 사용해서 다른 논리 게이트를 만든다.
  • 진리표를 보고 HDL 연결을 생각한다.
  • Mux와 DMux가 데이터를 선택하고 분배하는 방식을 이해한다.

NAND는 두 입력이 모두 1일 때만 0을 출력한다. 단순해 보이지만 입력을 같은 선에 연결하면 NOT이 되고, NOT을 다시 조합하면 AND와 OR도 만들 수 있다.

Not(a) = Nand(a, a)
And(a, b) = Not(Nand(a, b))
Or(a, b) = Nand(Not(a), Not(b))

Mux는 sel에 따라 a 또는 b를 선택한다. DMux는 반대로 하나의 입력을 a 또는 b 중 한 곳으로 보낸다.

1장 코드 보기

확인 문제

  1. NAND의 두 입력에 같은 값을 넣으면 왜 NOT이 되는가?
  2. Mux와 DMux의 차이를 데이터의 방향으로 설명해보자.
  3. Mux16은 1비트 Mux와 무엇이 같은가?

2장. 불 산술과 ALU

목표

  • 반가산기와 전가산기의 차이를 이해한다.
  • 여러 비트의 덧셈에서 carry가 전달되는 과정을 본다.
  • 제어 비트로 여러 연산을 수행하는 ALU를 이해한다.

반가산기는 두 비트를 더해서 sum과 carry를 만든다. 전가산기는 이전 자리에서 넘어온 carry까지 입력으로 받는다.

HalfAdder: a + b
FullAdder: a + b + carry-in
Add16: FullAdder를 연결한 16비트 덧셈기

ALU의 핵심은 연산마다 별도의 회로를 만드는 것이 아니다. zx, nx, zy, ny, f, no 신호로 입력과 출력을 바꾸면서 하나의 회로를 여러 방식으로 사용한다.

2장 코드 보기

확인 문제

  1. XOR가 덧셈의 sum으로 사용되는 이유는 무엇인가?
  2. 16비트 덧셈에서 마지막 carry를 버리면 어떤 일이 생기는가?
  3. ALU의 zr, ng 출력은 다음 장의 어떤 기능에 사용될까?

3장. 순차 논리와 메모리

목표

  • 조합 논리와 순차 논리의 차이를 이해한다.
  • DFF가 시간을 저장하는 최소 단위라는 점을 이해한다.
  • Bit, Register, RAM, PC가 어떻게 확장되는지 본다.

1장과 2장의 회로는 현재 입력만으로 출력이 결정된다. 하지만 메모리는 이전 클록의 값을 기억해야 한다.

DFF의 출력은 현재 입력이 아니라 이전 시점의 입력이다. 여기에 Mux를 붙이면 load가 1일 때만 새 값을 저장하는 Bit를 만들 수 있다.

DFF → Bit → Register → RAM8 → RAM64 → RAM512 → RAM4K → RAM16K

PC는 reset, load, inc의 우선순위가 중요하다. 보통 reset이 가장 강하고, 그다음 load, 마지막이 inc이다.

3장 코드 보기

확인 문제

  1. load가 0일 때 Bit가 기존 값을 유지하는 과정을 Mux로 설명해보자.
  2. RAM8에서 DMux와 Mux가 각각 필요한 이유는 무엇인가?
  3. PC의 제어 신호 우선순위가 뒤바뀌면 어떤 버그가 생길까?

4장. Hack 기계어

목표

  • A 명령어와 C 명령어를 구분한다.
  • A, D, M 레지스터의 역할을 이해한다.
  • 분기와 반복문이 어셈블리에서 어떻게 표현되는지 본다.

A 명령어는 값을 A 레지스터에 넣는다.

@10

C 명령어는 계산, 저장 위치, 점프 조건을 한 줄에 표현한다.

D=M+1;JGT

M은 별도 레지스터가 아니라 RAM[A]를 의미한다. 이 점을 놓치면 A 레지스터를 값으로 쓰는지 주소로 쓰는지 헷갈리기 쉽다.

4장 코드 보기

실습

  1. RAM[2] = RAM[0] + RAM[1]을 작성한다.
  2. 1부터 10까지 더하는 반복문을 작성한다.
  3. Fill 코드에서 SCREEN 주소가 왜 16384인지 확인한다.

5장. Hack 컴퓨터 구조

목표

  • CPU, Memory, ROM32K를 하나의 Computer로 연결한다.
  • 명령어의 비트가 ALU와 레지스터를 어떻게 제어하는지 이해한다.
  • fetch와 execute가 반복되는 구조를 본다.

CPU는 현재 명령어를 해석하고 ALU 결과를 A, D, M 중 어디에 저장할지 결정한다. 점프 조건이 참이면 PC에 A 레지스터 값을 넣고, 아니면 다음 명령어로 이동한다.

Memory는 주소 범위에 따라 RAM16K, Screen, Keyboard를 선택한다. 즉 메모리 주소를 읽고 쓰는 것만으로 화면과 키보드도 다룰 수 있다.

5장 코드 보기

확인 문제

  1. instruction[15]로 A/C 명령어를 구분하는 이유는 무엇인가?
  2. writeM이 1일 때 실제로 어느 주소에 값이 저장되는가?
  3. 키보드는 왜 load 신호가 필요하지 않은가?

6장. Hack 어셈블러

목표

  • .asm 파일을 16비트 .hack 파일로 변환한다.
  • 레이블과 변수 심볼을 처리한다.
  • 두 번 읽는 방식이 필요한 이유를 이해한다.

레이블은 실제 명령어가 아니므로 ROM 주소를 증가시키면 안 된다. 따라서 첫 번째 패스에서 (LOOP) 같은 레이블의 주소를 수집하고, 두 번째 패스에서 실제 명령어를 변환한다.

1차 패스: 레이블 → ROM 주소
2차 패스: A/C 명령어 → 16비트 코드
변수 심볼: RAM[16]부터 순서대로 할당

6장 코드 보기

실습

  1. 공백과 주석을 제거하는 전처리 함수를 작성한다.
  2. 같은 변수가 여러 번 나오더라도 주소를 한 번만 할당하는지 확인한다.
  3. dest, comp, jump 테이블에 없는 입력을 만났을 때 오류를 출력한다.

7장. VM 번역기 I

목표

  • 스택 기반 가상 머신을 이해한다.
  • 산술 명령어와 메모리 세그먼트를 Hack 어셈블리로 바꾼다.
  • 비교 연산에서 고유 레이블이 필요한 이유를 이해한다.

VM 명령어는 대부분 스택의 값을 꺼내 계산한 뒤 결과를 다시 넣는다.

push constant 7
push constant 8
add

local, argument, this, that은 기준 주소를 따라가고, temp, pointer, static은 각자 정해진 주소 규칙을 사용한다.

7장 코드 보기

확인 문제

  1. push와 pop이 SP를 변경하는 순서가 다른 이유는 무엇인가?
  2. eq, gt, lt마다 고유 레이블을 만들지 않으면 어떤 일이 생기는가?
  3. static 변수 이름에 파일명을 포함해야 하는 이유는 무엇인가?

8장. VM 번역기 II

목표

  • label, goto, if-goto를 함수 범위 안에서 처리한다.
  • function, call, return의 스택 프레임을 이해한다.
  • bootstrap 코드가 하는 일을 이해한다.

함수를 호출할 때는 돌아올 주소와 현재 프레임 상태를 저장해야 한다.

return-address
LCL
ARG
THIS
THAT

return은 FRAME = LCL, RET = *(FRAME - 5)를 먼저 보관한 뒤 호출자의 세그먼트를 복구한다. 복구 도중 LCL이 바뀌므로 RET를 먼저 저장해야 한다.

8장 코드 보기

실습

  1. 재귀 함수 호출에서 return label이 겹치지 않는지 확인한다.
  2. 여러 .vm 파일의 static 세그먼트가 충돌하지 않는지 확인한다.
  3. bootstrap에서 SP를 256으로 설정하고 Sys.init을 호출한다.

9장. Jack 고급 언어

목표

  • Jack의 class, constructor, method, function을 구분한다.
  • 객체와 배열이 메모리에 저장되는 방식을 이해한다.
  • OS API를 사용해서 입력과 출력을 처리한다.

Jack은 Java와 비슷해 보이지만 기능이 훨씬 단순하다. 객체의 field는 힙에 저장되고, method에서는 현재 객체의 주소가 this가 된다.

constructor는 필요한 크기만큼 메모리를 할당한 뒤 this를 반환한다. 배열도 특별한 자료구조라기보다 연속된 메모리 블록에 가깝다.

9장 코드 보기

실습

  1. Counter 객체를 두 개 만들고 값이 따로 유지되는지 확인한다.
  2. 키 입력이 계속 눌린 상태에서 중복 처리되지 않게 만든다.
  3. 사용이 끝난 객체에 dispose가 필요한 이유를 설명한다.

10장. Jack 구문 분석기

목표

  • tokenizer가 소스 코드를 토큰으로 나누는 과정을 이해한다.
  • Jack 문법을 재귀 하강 파서로 구현한다.
  • 파스 트리를 XML로 출력한다.

문자열 안의 기호와 주석 밖의 기호는 다르게 처리해야 한다. 따라서 단순히 공백으로 split하면 올바른 tokenizer를 만들 수 없다.

파서는 현재 토큰을 보고 어떤 문법 함수를 호출할지 결정한다.

class
 ├─ classVarDec
 ├─ subroutineDec
 └─ statements

10장 코드 보기

확인 문제

  1. 문자열 안의 //를 주석으로 지우면 왜 안 되는가?
  2. expression과 term을 분리하면 어떤 점이 편한가?
  3. 토큰을 consume하기 전에 예상 타입을 검사하면 무엇이 좋아지는가?

11장. Jack 컴파일러

목표

  • 파스 트리를 VM 명령어로 변환한다.
  • 클래스 범위와 서브루틴 범위 심볼을 구분한다.
  • 메서드 호출과 생성자 호출의 숨은 동작을 이해한다.

심볼 테이블은 이름만 저장하지 않는다. 타입, 종류, 인덱스를 함께 저장해서 어느 VM 세그먼트를 사용할지 결정한다.

static   → static
field    → this
argument → argument
var      → local

method는 첫 번째 argument로 현재 객체를 받고 pointer 0에 연결한다. constructor는 field 개수만큼 Memory.alloc을 호출한 뒤 반환 주소를 pointer 0에 넣는다.

11장 코드 보기

실습

  1. 같은 이름의 지역 변수가 field를 가릴 때 어떤 심볼이 선택되는지 확인한다.
  2. 배열 대입에서 주소와 값을 어떤 순서로 스택에 보관해야 하는지 정리한다.
  3. do 문의 반환값을 temp 0으로 버리는 이유를 설명한다.

12장. Jack 운영체제

목표

  • Jack 프로그램이 사용하는 기본 라이브러리를 직접 구현한다.
  • Math, String, Array, Memory의 책임을 구분한다.
  • 하드웨어와 고급 언어 사이의 마지막 빈칸을 채운다.

운영체제라고 해서 거대한 커널부터 만드는 것은 아니다. Jack 프로그램이 기대하는 함수들을 Jack 언어로 구현하는 것이 핵심이다.

  • Math: 곱셈, 나눗셈, 절댓값
  • String: 문자 저장, 길이, 출력 변환
  • Array: 연속 메모리 할당
  • Memory: 힙 할당과 해제
  • Screen: 픽셀과 도형 출력
  • Keyboard: 키 입력
  • Output: 문자와 숫자 출력
  • Sys: 초기화, 대기, 종료

12장 코드 보기

확인 문제

  1. 곱셈과 나눗셈을 CPU 명령어 없이 구현할 수 있는 이유는 무엇인가?
  2. String 객체가 length와 capacity를 따로 가지는 이유는 무엇인가?
  3. 메모리 해제를 하지 않으면 Jack 프로그램에서 어떤 문제가 생기는가?

추천 학습 순서

각 장은 다음 순서로 진행하는 것이 좋다.

  1. 완성 코드를 보기 전에 입출력과 진리표를 먼저 적는다.
  2. 작은 부품부터 테스트한다.
  3. 테스트가 통과하면 다음 단계의 부품으로 사용한다.
  4. 실패하면 출력만 보지 말고 어느 계층에서 값이 달라졌는지 찾는다.
  5. 마지막에 기존 블로그 코드를 자신의 코드와 비교한다.

정답 코드를 그대로 입력하면 프로젝트는 빨리 끝난다. 하지만 Nand2Tetris에서 중요한 것은 정답 자체보다 한 단계 아래 계층이 어떻게 현재 계층을 지탱하는지 이해하는 것이다.

NAND 하나가 운영체제까지 이어진다는 흐름이 보이면, 컴퓨터가 더 이상 마법처럼 보이지 않게 된다.