NARU / DOCS / DATABASE / SDK 1.0.0

정적 웹사이트에 데이터를 더하세요.

별도 서버 없이 글을 공개하고, 방문자의 인사를 받고, 내 웹사이트에서 글을 작성하세요.

01 · 빠른 시작

제어판에서 posts 컬렉션을 만들고 읽기를 공개로 바꾸세요. 제목이 있는 문서를 하나 저장한 뒤 페이지에 아래 코드를 넣으세요.

JSON
{
  "title": "첫 번째 글"
}
HTML
<ul id="posts"></ul>
<script type="module">
  import { createNaru } from "https://naru.pub/sdk/1/naru.js";

  const naru = createNaru();
  const { documents } = await naru.collection("posts").list();
  for (const post of documents) {
    const item = document.createElement("li");
    item.textContent = post.data.title;
    document.querySelector("#posts").append(item);
  }
</script>

방명록에 인사 남기기

제어판에서 guestbook 컬렉션을 만들고 읽기는 ‘누구나’, 쓰기는 ‘누구나 생성만’으로 설정하세요.

HTML
<button id="hello">인사 남기기</button>
<p id="result"></p>
<script type="module">
  import { createNaru } from "https://naru.pub/sdk/1/naru.js";
  const naru = createNaru();
  const button = document.querySelector("#hello");
  const result = document.querySelector("#result");
  button.onclick = async () => {
    button.disabled = true;
    try {
      const saved = await naru.collection("guestbook").add({
        name: "방문자", message: "안녕하세요!",
      });
      result.textContent = saved.data.message + " 저장됨";
    } catch (error) {
      result.textContent = "결과를 확인하지 못했습니다. 방명록을 확인하세요.";
      console.error(error);
    }
  };
</script>

02 · 컬렉션과 공개 범위

컬렉션은 문서를 모으는 곳이고, 문서는 ID 하나와 JSON 데이터 하나입니다. 이름과 문서 ID는 영문·숫자·_·- 1~64자입니다.

설정방문자가 할 수 있는 일
읽기관리자만방문자는 읽을 수 없습니다.
읽기누구나get·list·count·pages로 누구나 읽습니다.
쓰기관리자만방문자는 쓸 수 없습니다.
쓰기누구나 생성만add로 새 문서만 만듭니다. 방명록에 알맞습니다.
쓰기누구나 생성·덮어쓰기·삭제누구나 모든 문서를 바꾸고 지울 수 있습니다. 대부분은 필요 없습니다.

관리자는 로그인하면 등록한 컬렉션을 공개 범위와 상관없이 읽고 쓸 수 있습니다. 공개 읽기 컬렉션의 문서는 누구나 읽을 수 있으니, 비공개로 둘 데이터는 ‘관리자만’ 읽는 컬렉션에 따로 저장하세요.

03 · 관리자 로그인

나루 홈페이지의 소유자가 로그인하면 홈페이지에 로그인할 수 있습니다.

  1. 제어판의 ‘웹사이트 관리자 로그인’에 관리자 페이지 주소(예: https://example.naru.pub/)와 쓸 컬렉션을 등록합니다.
  2. naru.auth.signIn()으로 로그인하고 naru.auth.session()으로 관리자 클라이언트를 받습니다.
HTML
<button id="login">관리자 로그인</button>
<p id="auth-status"></p>
<script type="module">
  import { createNaru } from "https://naru.pub/sdk/1/naru.js";
  const naru = createNaru();
  const admin = await naru.auth.session();
  const login = document.querySelector("#login");
  login.hidden = !!admin;
  document.querySelector("#auth-status").textContent =
    admin ? "로그인되었습니다." : "글을 고치려면 로그인하세요.";
  login.onclick = () => naru.auth.signIn({ collections: ["posts", "drafts"] });
  // 다음 예제의 owner는 이 스크립트 안에서 사용하세요.
</script>

04 · 글 수정과 충돌 처리

호출동작
add(data)새 ID로 문서를 만듭니다.
set(id, data, { condition })그 ID의 문서를 만들거나 통째로 바꿉니다. 필드를 합치지 않습니다.
delete(id, { condition })문서를 지웁니다. 없는 문서를 지워도 성공합니다.

condition으로 덮어쓰기를 막을 수 있습니다. { revision }은 읽은 뒤 바뀌지 않았을 때만, { absent: true }는 아직 없는 문서일 때만 저장하고, 아니면 CONFLICT로 실패합니다.

JavaScript
const post = await admin.collection("posts").get("hello");
try {
  await admin.collection("posts").set(
    "hello",
    { ...post.data, title: "새 제목" },
    { condition: { revision: post.revision } },
  );
} catch (error) {
  if (error.code !== "CONFLICT") throw error;
  alert("다른 곳에서 먼저 저장했습니다. 새로고침 후 다시 시도하세요.");
}

데이터에는 JSON 값만 넣으세요.

여러 변경을 함께 저장하기

admin.batch()은 여러 컬렉션의 set·delete를 최대 100개까지 묶어, 원자적으로 반영합니다. 각 변경의 condition도 함께 검사합니다. 미리 읽은 값이 저절로 보호되는 것은 아니므로 고치는 문서의 revision을 넘기세요. 결과는 쓴 순서대로, set은 { id, revision, createdAt, updatedAt }, delete는 null입니다.

JavaScript
const [saved] = await admin.batch([
  { collection: "posts", set: { id, data: post, condition: { absent: true } } },
  { collection: "drafts", delete: { id, condition: { revision: draft.revision } } },
]);
// saved.revision → 다음 저장의 condition

05 · 목록과 페이지 나누기

JavaScript
const posts = naru.collection("posts");

const post = await posts.get("hello");
// → { id, data, revision, createdAt, updatedAt }

const query = {
  filter: { category: "일상", date: { gte: "2026-09-01" } },
  sort: [["date", "desc"]],
};
const page = await posts.list({ ...query, size: 20 });
// → { documents, nextCursor }

if (page.nextCursor) {
  const next = await posts.list({ ...query, size: 20, after: page.nextCursor });
  console.log(next.documents);
}

// 조건에 맞는 문서 수
const total = await posts.count({ filter: query.filter });

// 끝까지 한 쪽씩 (nextCursor를 대신 따라갑니다)
for await (const { documents } of posts.pages({ ...query, size: 100 })) {
  console.log(documents);
}
  • filter: 최상위 필드를 값으로 비교하거나 { gt, gte, lt, lte }로 범위를 찾습니다. 조건은 모두 만족해야 하며(AND) 최대 5개입니다. 날짜는 "2026-09-01"처럼 자리를 채운 문자열로 저장하면 범위로 찾을 수 있습니다.
  • sort: [필드, 방향]을 한두 개 넘깁니다. 생성·수정 시각은 { metadata: "createdAt" }처럼 씁니다. 기본은 ID 오름차순입니다. 같은 날짜에서 최신 글을 먼저 보려면[["date", "desc"], [{ metadata: "createdAt" }, "desc"]]처럼 두 키를 씁니다. 문자열은 언어별 정렬 없이 유니코드 순서로, 숫자는 크기로 비교합니다. 오름차순에서 없는 값·null·배열·객체, 문자열, 숫자, 불리언 순입니다.
  • size: 한 번에 기본 50개, 최대 100개입니다. 다음 페이지는 같은 filter·sort에 nextCursor를 after로 넘기고, nextCursor가 null이면 끝입니다. includeTotal: true면 전체 개수도 받습니다. 개수만 필요하면 count()를, 모든 쪽이 필요하면 pages()를 쓰세요. 누구나 쓸 수 있는 컬렉션은 끝까지 읽지 말고 필요한 만큼만 받으세요.

06 · 사용 예

블로그 글 공개

posts · 읽기 누구나 / 쓰기 관리자만

naru
  .collection("posts")
  .list({
    sort: [["date", "desc"]],
  });

방명록

guestbook · 읽기 누구나 / 쓰기 누구나 생성만

naru.collection("guestbook")
  .add({ name, message });

비공개 문의 받기

inquiries · 읽기 관리자만 / 쓰기 누구나 생성만

naru.collection("inquiries")
  .add({ email, message });

초안 저장 후 공개

drafts · 읽기 관리자만 / 쓰기 관리자만

admin.batch([
  { collection: "posts",
    set: { id, data, condition: { absent: true } } },
  { collection: "drafts",
    delete: { id, condition: { revision: draft.revision } } },
]);

글에 이미지 붙이기

미디어 라이브러리 · 관리자 업로드

const image =
  await admin.media.upload(file);
const posts = admin.collection("posts");
const post = await posts.get(id);
await posts.set(id, { ...post.data, cover: image.url },
  { condition: { revision: post.revision } });

특정 글의 댓글

comments · 읽기 누구나 / 쓰기 누구나 생성만

naru
  .collection("comments")
  .list({
    filter: { postId: "hello" },
  });

예제 블로그 ‘작은 기록’

글 목록·상세·방명록·관리자 편집·비공개 초안을 갖춘 정적 사이트입니다. ZIP 내려받기 · 실제 사이트 보기 ↗

  1. 제어판에서 posts, guestbook, drafts 컬렉션을 위 설정대로 만듭니다.
  2. ‘웹사이트 관리자 로그인’에 올릴 위치의 admin/ 주소와 posts·drafts를 등록합니다.
  3. 연결한 도메인에 올린다면 config.js의 site에 로그인 이름을 적습니다.
  4. 파일을 새 폴더에 올리고 index.html을 열어 글을 써 봅니다.

07 · 한도와 오류

  • 사이트당 컬렉션 100개, 문서 10,000개, 데이터 10 MiB. 요청 하나는 64 KiB까지입니다.
  • 방문자의 add는 분당 횟수가 제한됩니다.
  • 관리자 로그인 페이지는 사이트당 20개까지 등록합니다.

실패하면 NaruError를 던집니다. code로 구분하세요.

code뜻
NOT_FOUND문서, 컬렉션 또는 사이트가 없습니다.
ACCESS_DENIED공개 범위나 등록된 컬렉션이 허용하지 않습니다.
AUTH_REQUIRED관리자 권한이 만료되었습니다. 다시 로그인하세요.
CONFLICTcondition이 맞지 않습니다.
QUOTA_EXCEEDED데이터나 미디어 저장 공간이 가득 찼습니다.
RATE_LIMITED요청이 너무 많습니다. 잠시 후 다시 시도하세요.
INVALID_REQUEST이름, 필터, 크기 등 요청 형식이 잘못되었습니다.
UNAVAILABLE일시적인 오류입니다.
JavaScript
try {
  await naru.collection("guestbook").add({ message });
} catch (error) {
  status.textContent =
    error.code === "RATE_LIMITED" ? "잠시 후 다시 시도하세요." : error.message;
}