Tree 모듈: SQLite에서 페이지가 행으로 변하는 곳

발행: (2026년 2월 6일 오전 06:16 GMT+9)
16 min read
원문: Dev.to

Source: Dev.to

트리 모듈: 페이지가 SQLite에서 행으로 변환되는 곳

SQLite는 가볍고 빠른 관계형 데이터베이스이지만, 복잡한 계층 구조 데이터를 다루는 데는 종종 한계가 있습니다. 특히 페이지‑기반 UI를 구현하면서 각 페이지를 별도의 행으로 저장하고 싶을 때, 전통적인 테이블 스키마만으로는 충분하지 않을 수 있습니다. 여기서는 Tree 모듈을 사용해 이러한 문제를 해결하는 방법을 살펴보겠습니다.

왜 Tree 모듈인가?

  • 계층 구조 지원: 부모‑자식 관계를 자연스럽게 표현할 수 있습니다.
  • 재귀 쿼리: WITH RECURSIVE 없이도 손쉽게 전체 트리를 탐색할 수 있습니다.
  • 정렬 및 위치 지정: 페이지 순서를 지정하는 position 컬럼을 활용해 UI에서 원하는 순서를 유지할 수 있습니다.

기본 스키마

아래는 트리 구조를 저장하기 위한 최소 스키마 예시입니다.

CREATE VIRTUAL TABLE pages USING tree(
    id INTEGER PRIMARY KEY,
    parent_id INTEGER,
    title TEXT,
    content TEXT,
    position INTEGER
);
  • id: 각 페이지를 고유하게 식별합니다.
  • parent_id: 상위 페이지의 id. 루트 페이지는 NULL입니다.
  • title / content: 페이지의 실제 데이터.
  • position: 같은 부모 아래에서의 정렬 순서.

페이지 삽입

INSERT INTO pages (parent_id, title, content, position)
VALUES (NULL, '홈', '홈 페이지 내용', 1);          -- 루트 페이지
INSERT INTO pages (parent_id, title, content, position)
VALUES (1, '소개', '소개 페이지 내용', 1);          -- '홈'의 자식
INSERT INTO pages (parent_id, title, content, position)
VALUES (1, '연락처', '연락처 페이지 내용', 2);    -- '홈'의 또 다른 자식

트리 전체 조회

tree 가상 테이블은 자동으로 pathdepth 컬럼을 제공합니다. 이를 이용해 전체 트리를 한 번에 가져올 수 있습니다.

SELECT id, parent_id, title, depth, path
FROM pages
ORDER BY path;
idparent_idtitledepthpath
1NULL01
21소개11/2
31연락처11/3
  • depth는 루트에서부터의 거리이며, UI에서 들여쓰기 수준을 결정하는 데 유용합니다.
  • path/ 로 구분된 ID 시퀀스로, 정렬에 사용됩니다.

특정 서브트리 가져오기

예를 들어, 페이지 아래의 모든 하위 페이지만 보고 싶다면:

SELECT *
FROM pages
WHERE path GLOB '1/*';

GLOB 연산자는 와일드카드 매칭을 제공해 지정된 루트 이하의 모든 노드를 반환합니다.

페이지 이동 (재배치)

페이지를 다른 부모 아래로 옮기거나 순서를 바꾸는 작업은 UPDATE 문 하나로 해결됩니다.

-- '연락처' 페이지를 '소개' 페이지 아래로 이동하고 position을 1로 설정
UPDATE pages
SET parent_id = 2,
    position = 1
WHERE id = 3;

업데이트 후 pathdepth가 자동으로 재계산됩니다.

삭제와 정리

노드를 삭제하면 해당 노드와 모든 자손이 함께 사라집니다.

DELETE FROM pages WHERE id = 2;  -- '소개'와 그 하위 노드 모두 삭제

실전 예제: 페이지 기반 블로그

아래는 간단한 블로그 시스템을 구현한 전체 스크립트입니다.

-- 테이블 생성
CREATE VIRTUAL TABLE blog_pages USING tree(
    id INTEGER PRIMARY KEY,
    parent_id INTEGER,
    title TEXT,
    body TEXT,
    position INTEGER
);

-- 루트(블로그 메인) 페이지
INSERT INTO blog_pages (parent_id, title, body, position)
VALUES (NULL, '블로그 메인', '환영합니다!', 1);

-- 카테고리
INSERT INTO blog_pages (parent_id, title, body, position)
VALUES (1, '프로그래밍', '', 1);
INSERT INTO blog_pages (parent_id, title, body, position)
VALUES (1, '여행', '', 2);

-- 포스트
INSERT INTO blog_pages (parent_id, title, body, position)
VALUES (2, 'SQLite 트리 모듈', 'SQLite에서 트리 구조를 다루는 방법...', 1);
INSERT INTO blog_pages (parent_id, title, body, position)
VALUES (3, '제주도 여행기', '제주도에서의 멋진 추억...', 1);

UI에서 활용하기

  • 목차: depth 값을 이용해 들여쓰기 레벨을 지정하고, title을 클릭하면 해당 id 로 상세 페이지를 로드합니다.
  • 드래그 앤 드롭: 페이지를 끌어다 놓을 때 parent_idposition 을 업데이트하면 트리 구조가 즉시 반영됩니다.
  • 검색: title 혹은 body 컬럼에 LIKE 혹은 FTS5 인덱스를 적용해 전체 트리에서 텍스트 검색이 가능합니다.

마무리

Tree 모듈은 SQLite에 계층형 데이터를 자연스럽게 저장하고 조작할 수 있는 강력한 도구입니다. 페이지 기반 애플리케이션, 메뉴 구조, 포럼 스레드 등 다양한 시나리오에 적용할 수 있습니다.

  • 장점: 별도의 재귀 CTE 없이도 트리 탐색이 가능하고, 삽입·삭제·이동이 간단합니다.
  • 주의점: 가상 테이블이므로 일반 테이블보다 약간의 오버헤드가 존재합니다. 대규모 데이터셋에서는 성능 테스트를 권장합니다.

SQLite와 Tree 모듈을 활용해 복잡한 UI 구조를 데이터베이스 수준에서 깔끔하게 관리해 보세요!

페이지에서 트리로

이전 장에서는 pager에서 멈췄습니다.
pager는 SQLite에 매우 중요한—하지만 매우 제한적인—것을 제공했습니다: 바이트 스트림에 대한 페이지‑지향 뷰.

페이지 17을 요청하고, 수정하고, 고정(pin)하고, 고정 해제(unpin)할 수 있었지만, pager는 , , 또는 레코드가 무엇인지 전혀 몰랐습니다.

그 격차가 바로 tree 모듈이 등장하는 지점입니다.

tree 모듈은 이 평면적인 페이지‑지향 세계를 VM이 이해할 수 있는 형태, 즉 튜플‑지향, 키‑순서 파일로 변환합니다.

이 시점부터 VM은 더 이상 페이지를 기준으로 생각하지 않습니다. 대신 튜플을 기준으로 생각합니다—정렬되고, 검색 가능하며, 삽입 가능한 데이터 단위.

SQLite에서 튜플이란 정확히 무엇인가요?

앞으로 진행하기 전에, 흔히 발생하는 혼동을 천천히 풀어보는 것이 좋습니다.

  • SQLite의 튜플은 데이터 타입이 아닙니다.
  • 또한 C 구조체도, 행 객체도 아니며, 트리 모듈이 의미적으로 해석하려고 하는 것도 아닙니다.

튜플은 단순히:

  1. VM이 보는 관계의 논리적 행이며, 내부적으로는 가변 길이 바이트 시퀀스로 표현됩니다.
  2. 트리 모듈에 의해 불투명 데이터로 취급됩니다.

다시 말해, 트리 모듈은 튜플이 다음 중 무엇을 나타내는지 관심을 두지 않습니다:

  • 컬럼이 있는 테이블 행, 혹은
  • 키 + rowid 로 구성된 인덱스 엔트리.

sqlite_master의 메타데이터는 별도로 저장됩니다. 트리 모듈에겐 이 모든 것이 연관된 키를 가진 바이트 문자열일 뿐입니다. 의미는 주로 VM과 레코드‑포맷 로직에 의해 트리 모듈 위에서 부여됩니다.

튜플이 조직이 필요한 이유

관계는 개념적으로 튜플 집합이지만, 집합만으로는 효율적인 접근이 충분하지 않습니다. VM은 다음과 같은 보장을 필요로 합니다:

  • 키로 튜플 찾기
  • 순서대로 튜플 스캔
  • 튜플을 효율적으로 삽입 및 삭제
  • 같은 파일에 존재하더라도 서로 다른 관계의 튜플을 구분

튜플을 조직하는 고전적인 방법에는 다음이 포함됩니다:

  • 엔트리 시퀀스(추가 전용)
  • 상대 위치 지정
  • 해시 기반 조직
  • 키 시퀀스 조직 (SQLite 선택)

각 선택은 삽입, 조회 및 삭제가 동작하는 방식에 영향을 미칩니다.

SQLite의 한 가지 큰 설계 결정: 모든 것이 트리다

SQLite는 영구 데이터에 대해 정확히 하나의 조직 기법만을 사용합니다:

구조사용 용도
B⁺‑트리테이블
B‑트리인덱스

해시 테이블도, 힙 파일도, 클러스터드 보조 구조도 없습니다. 테이블이든 인덱스든 각 관계마다 자체 트리를 가집니다.

결과

  • 각 트리는 하나의 관계 또는 인덱스에 해당합니다.
  • 모든 트리는 하나의 데이터베이스 파일 안에 존재합니다.
    • 서로 다른 트리의 페이지가 디스크 상에서 뒤섞일 수 있습니다.
    • 단일 페이지가 여러 트리의 튜플을 동시에 포함하는 일은 절대 없습니다.

이 마지막 점이 핵심입니다. 파일 수준에서는 페이지가 공유되지만, 소유권은 트리 수준에서 엄격히 구분됩니다. 이를 강제하는 것이 트리 모듈의 주요 책임 중 하나입니다.

페이지 지향에서 트리 지향으로

레이어노출되는 내용
Pager번호가 매겨진 페이지들의 컬렉션
Tree Module순차 접근과 튜플 수준 삽입, 삭제, 조회를 지원하는 트리‑지향 파일

내부적으로, 트리 모듈은:

  • 어떤 페이지가 어떤 트리에 속하는지 선택합니다
  • 부모/자식 관계를 유지합니다
  • B⁺‑트리 균형 속성을 보장합니다
  • 페이지가 분할되거나 병합될 시점을 결정합니다

Pager에게 페이지는 단순히 페이지일 뿐입니다. 트리 모듈에게 페이지는 노드입니다.

트리 모듈은 의도적으로 수동적이다

One subtle but important design choice: the tree module is 구조적으로는 활성화되어 있지만 의미적으로는 수동적.

What it does care aboutWhat it does not care about
Keys, ordering, page layout, tree balance, navigationColumn types, SQL schemas, constraints, meanings of fields inside a tuple
관심 있는 항목관심 없는 항목
키, 정렬, 페이지 레이아웃, 트리 균형, 탐색컬럼 타입, SQL 스키마, 제약조건, 튜플 내부 필드의 의미

It simply stores and retrieves variable‑length byte strings and keeps them in order. This separation of concerns allows SQLite’s VM to evolve independently of its storage engine.

트리 메타데이터가 저장되는 위치

SQLite는 여전히 다음과 같은 질문에 답할 방법이 필요합니다:

  • 어떤 트리가 어느 테이블에 속하는가?
  • 이 트리의 루트 페이지는 어디에 있는가?
  • 이 트리가 테이블인지 인덱스인지?

그 매핑 정보는 특별하고 잘 알려진 관계에 저장됩니다:

  • sqlite_master (임시 객체의 경우 sqlite_temp_master)

이 카탈로그 자체도 다른 모든 것처럼 미리 정해진 B⁺‑트리에 저장됩니다. SQLite는 메타데이터를 “시스템 외부”에 저장함으로써 속이지 않으며—자신이 만든 것을 직접 사용합니다.

다음에 우리가 나아갈 방향

지금까지 큰 그림을 정리했습니다:

  • Pager → 페이지
  • Tree module → 튜플
  • VM → SQL 의미론

다음 며칠 동안은 더 자세히 살펴보고 구체화할 예정입니다:

  1. 트리 인터페이스 함수 – VM이 트리와 실제로 통신하는 방법
  2. B⁺‑Tree 구조 – 내부 노드, 리프 노드, 구분자
  3. 페이지 구조 – 튜플이 하나의 페이지 안에서 어떻게 패킹되고, 인덱싱되며, 탐색되는지

이때부터 다이어그램이 의미를 갖기 시작하고, SQLite 설계의 진정한 우아함이 드러납니다.

앞으로 기억할 핵심 아이디어: SQLite는 바이트 파일을 페이지로, 페이지를 트리로, 트리를 관계로 변환합니다—한 번에 한 층씩 깔끔하게.

제 실험과 SQLite에 대한 직접 실행 예제는 여기에서 확인할 수 있습니다:
lovestaco/sqlite – sqlite‑examples

참고 문헌

[![FreeDevTools Screenshot](https://media2.dev.to/dynamic/image/width=800,height=,fit=scale-down,gravity=auto,format=auto/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o5e57lh7rtwwwe2d694n.png)](https://hexmos.com/freedevtools/)

👉 **Check out:** [FreeDevTools](https://hexmos.com/freedevtools/)

Any feedback or contributions are welcome!  
It’s online, open‑source, and ready for anyone to use.

**Star it on GitHub:** [FreeDevTools repository](https://github.com/HexmosTech/FreeDevTools)

👉 확인해 보세요: FreeDevTools

피드백이나 기여를 환영합니다!
온라인이며, 오픈 소스이고, 누구나 사용할 수 있습니다.

GitHub에서 별표 달기: FreeDevTools repository

Back to Blog

관련 글

더 보기 »