본문 바로가기
WIKI 기술 지식 베이스

커스텀 오브젝트 뷰어

원문 보기 위키 갱신

커스텀 오브젝트 뷰어 (Custom Object Viewer)

커스텀 오브젝트 뷰어는 사용자가 조건에 맞는 객체를 열었을 때 lakeFS가 끼워 넣는(임베드하는) 웹 페이지예요. lakeFS가 렌더링하지 않는 파일 형식을 보여 주거나, 기본 제공 뷰어를 자신만의 뷰어로 교체할 때 써요. 뷰어는 확장자나 콘텐츠 타입으로 객체에 매칭돼요. 자세한 규칙은 Matching rules 섹션을 보세요.

출처: 문서

본문

lakeFS Team과 lakeFS Enterprise에서 사용할 수 있어요. 무료 평가판을 시작하거나 문의하세요.

참고

메시징 프로토콜은 실험적(experimental)이며 릴리스 사이에 바뀔 수 있어요.

사전 준비물

시작하기 전에 필요한 것:

  • lakeFS Enterprise 버전 1.93.0 이상

  • 뷰어를 등록할 lakeFS 서버 설정을 편집할 권한

  • 새 설정을 로드하도록 lakeFS를 재시작할 권한

  • 정적 HTML 페이지를 호스팅할 곳 — 각 사용자의 브라우저에서 접근 가능해야 하고, 호스팅 요구 사항을 충족해야 해요.

아래 워크스루는 Python 3에 내장된 HTTP 서버로 페이지를 로컬에서 서빙하지만, 어떤 정적 HTTP 서버라도 괜찮아요.

텍스트 뷰어 만들기

1. 페이지 만들기

아래 내용을 index.html로 저장하세요:

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Text viewer</title>
  </head>
  <body>
    <pre id="output"></pre>

    <script>
      window.addEventListener("message", (event) => {
        // Ignore messages that are not from the embedding lakeFS page.
        if (event.source !== window.parent) return;
        if (event.data?.type !== "object-response") return;

        const text = new TextDecoder().decode(event.data.content);
        document.getElementById("output").textContent = text;

        const height = document.documentElement.scrollHeight + "px";
        window.parent.postMessage({ type: "set-iframe-height", height }, "*");
      });

      // Register the listener before telling lakeFS that the viewer is ready.
      window.parent.postMessage({ type: "viewer-ready" }, "*");
    </script>
  </body>
</html>

뷰어가 viewer-ready를 보내면 lakeFS는 object-response로 객체를 돌려 줘요. 이 예시는 콘텐츠를 UTF-8로 디코드하고 텍스트에 맞춰 프레임 크기를 조정해요.

2. 페이지 서빙하기

index.html이 있는 디렉터리에서 아래 명령을 실행해요:

python3 -m http.server 8080

페이지는 http://localhost:8080/에서 서빙돼요. 또는 index.html을 아무 정적 파일 서버에 복사하고 그 URL을 써도 돼요.

3. 뷰어 등록하기

lakeFS 서버 설정의 ui.custom_viewers에 뷰어를 추가하고, url에 페이지를 서빙하는 주소를 넣으세요:

ui:
  custom_viewers:
    - name: Text viewer
      url: http://localhost:8080/
      extensions:
        - txt
      content_types:
        - text/plain

새 설정을 로드하도록 lakeFS를 재시작하세요.

4. 뷰어 테스트하기

알려진 텍스트를 담은 hello.txt 파일을 업로드하고, 저장소의 objects 페이지에서 열어 보세요. 커스텀 뷰어에 내용이 표시되면 성공이에요.

기본 뷰어가 열리거나 프레임이 빈 채로 남아 있다면 Troubleshooting 섹션을 참고하세요.

다른 사용자에게 뷰어를 공개하기 전에 Hosting requirements와 Security 섹션을 꼭 검토하세요.

이미지 표시하기

텍스트 뷰어의 <script> 요소를 아래 스크립트로 바꿔요. ArrayBuffer로 Blob을 만들고 객체 URL을 통해 표시해요:

window.addEventListener("message", (event) => {
  if (event.source !== window.parent) return;
  if (event.data?.type !== "object-response") return;

  const blob = new Blob([event.data.content], { type: event.data.contentType });
  const image = document.createElement("img");
  image.style.maxWidth = "100%";
  image.onload = () => {
    const height = document.documentElement.scrollHeight + "px";
    window.parent.postMessage({ type: "set-iframe-height", height }, "*");
  };
  image.src = URL.createObjectURL(blob);
  document.body.appendChild(image);
});

window.parent.postMessage({ type: "viewer-ready" }, "*");

이미지의 콘텐츠 타입이나 확장자에 맞게 뷰어 설정을 업데이트하세요.

큰 객체의 일부만 읽기

기본적으로 lakeFS는 객체 전체를 보내요. 필요한 바이트만 읽고 싶다면 metadataOnly: true를 넣어 viewer-ready를 보낸 뒤, range-request 메시지를 사용해요.

텍스트 뷰어의 <script> 요소를 아래 스크립트로 바꾸면, 큰 텍스트 객체의 끝부분을 tail처럼 보여 줘요:

const output = document.getElementById("output");

window.addEventListener("message", (event) => {
  if (event.source !== window.parent) return;

  switch (event.data?.type) {
    case "object-response": {
      const rangeSize = Math.min(1024, event.data.size);
      if (rangeSize > 0) {
        const start = event.data.size - rangeSize;
        const message = { type: "range-request", id: "tail", start, rangeSize };
        window.parent.postMessage(message, "*");
      }
      break;
    }
    case "range-response": {
      const text = new TextDecoder().decode(event.data.content);
      // A range starts mid-line, so drop the partial first line.
      output.textContent = text.slice(text.indexOf("\n") + 1);
      break;
    }
    case "range-error":
      output.textContent = event.data.error;
      break;
  }
});

window.parent.postMessage({ type: "viewer-ready", metadataOnly: true }, "*");

요청을 두 개 이상 보낼 때는 각 요청에 고유한 id를 주세요. Parquet처럼 메타데이터가 파일 푸터에 들어 있는 포맷에는 range 읽기가 잘 맞아요.

동작 방식

사용자가 객체를 열 때마다 lakeFS는 새 샌드박스 iframe에 페이지를 로드해요. 페이지와 lakeFS는 window.postMessage로 객체 데이터를 주고받아요. 프로토콜 레퍼런스에 모든 메시지가 정리되어 있어요.

매칭 규칙

사용자가 객체를 열면 lakeFS는 ui.custom_viewers에 설정된 값을 가지고 두 단계로 뷰어를 고름:

  • 객체 이름에서 마지막 점 뒤의 텍스트를 소문자로 바꾸고, extensions에 그 값을 나열한 뷰어를 찾아요.

  • 매칭이 없으면, content_types에 객체의 콘텐츠 타입을 나열한 뷰어를 찾아요.

콘텐츠 타입이나 확장자는 한 뷰어에만 속할 수 있어요. 어느 뷰어도 매칭되지 않으면 lakeFS의 기본 렌더링으로 돌아가요.

샌드박싱

프레임에는 sandbox="allow-scripts"가 걸려 있어서, 뷰어는 스크립트를 실행할 수 있지만 자신을 서빙하는 호스트의 쿠키나 브라우저 스토리지는 읽지 못해요. 샌드박스는 페이지에 불투명(opaque) origin을 부여하기 때문에, 자기 호스트로 보내는 요청조차 교차 출처(cross-origin)가 돼요.

호스팅 요구 사항

뷰어 URL은 각 사용자의 브라우저에서 접근 가능해야 해요. lakeFS UI가 HTTPS라면 뷰어도 HTTPS로 서빙하세요. 브라우저는 HTTPS 페이지 안의 HTTP 뷰어를 차단해요. http://localhost만 예외예요.

호스트에 걸린 Content-Security-Policy나 X-Frame-Options 헤더는 lakeFS origin이 페이지를 임베드할 수 있게 허용해야 해요.

뷰어 호스트에서 CORS 허용하기

뷰어가 모듈 스크립트나 폰트를 로드하거나, fetch나 XMLHttpRequest로 응답을 읽는다면 뷰어 호스트에서 Access-Control-Allow-Origin: *을 반환하세요. 뷰어가 불투명 origin에서 동작하기 때문에 이 요청들은 교차 출처예요. 호스트의 CORS 설정을 바꿀 수 없다면, 대신 샌드박스를 완화해요.

보안

뷰어는 스크립트를 실행할 수 있고, 사용자가 여는 모든 매칭 객체의 전체 내용을 받아요. 그러니 코드와 호스팅을 신뢰할 수 있는 뷰어만 등록하세요. 뷰어는 lakeFS 자격 증명을 갖지 않고, 기본적으로 lakeFS API를 통해 무언가를 읽거나 바꿀 수 없어요.

객체 내용은 사용자 데이터이므로, textContent나 <img> 요소 같은 안전한 DOM API로 렌더링하세요.

시작 후에는 페이지를 벗어나 탐색하지 마세요. lakeFS는 프레임으로 응답을 보내기 때문에, 프레임을 교체한 페이지가 객체 데이터를 받고 추가 요청을 보낼 수 있게 돼요.

allow_same_origin으로 샌드박스 완화하기

allow_same_origin: true를 쓰면 뷰어가 실제 origin을 유지해요. 자기 호스트로의 요청이 더 이상 교차 출처가 아니라서 CORS 없이 리소스를 로드할 수 있죠. 단, lakeFS는 lakeFS origin 자체에서 로드되는 이런 뷰어를 거부해요.

가능하면 뷰어 호스트에 CORS를 설정하는 쪽을 선호하세요.

경고

뷰어는 lakeFS의 형제 서브도메인이 아니라 자체 도메인에 호스팅하세요. viewer.example.com과 lakefs.example.com은 같은 사이트(same site)라서, 뷰어가 lakeFS로 보내는 요청에 사용자의 세션 쿠키가 실릴 수 있어요.

문제 해결

lakeFS는 뷰어 오류를 프레임 위에 표시해요. 이 오류들은 뷰어로 전송되지 않아요.

  • 기본 뷰어가 열려요. 객체가 설정된 콘텐츠 타입이나 확장자에 매칭되지 않았어요. Matching rules를 참고하세요.

  • 프레임이 빈 채로 남아요. 다음을 확인하세요:

    • 뷰어 URL을 직접 열어서 로드되는지 확인하세요. lakeFS 밖에서는 객체 내용을 표시하지 않아요.

    • lakeFS 탭의 브라우저 개발자 도구를 열고 Console 패널에서 프레임 차단, HTTPS 페이지 안의 HTTP 뷰어, CORS 오류를 확인하세요. Hosting requirements를 참고하세요.

    • 뷰어가 viewer-ready를 보내기 전에 메시지 리스너를 등록하는지 확인하세요.

  • 변경 사항이 보이지 않아요. 뷰어 페이지의 변경을 반영하려면 저장소의 objects 페이지를 새로 고치세요. 설정 변경은 lakeFS 재시작이 필요해요.

프로토콜 레퍼런스

모든 메시지는 필수 type 필드를 가진 일반 객체예요. 아래 각 헤딩이 그 값이고, 그 아래 코드 블록이 메시지의 모양을 보여 줘요. 숫자 필드는 유한한 safe integer여야 해요.

lakeFS는 데이터가 객체가 아닌 메시지를 조용히 무시해요. 알 수 없는 메시지 타입과 필드가 잘못된 알려진 타입은 뷰어로 전달되지 않고 lakeFS UI에만 보고돼요. Troubleshooting를 참고하세요.

메시지 흐름

전형적인 교환은 뷰어가 viewer-ready를 보내면서 시작해요.

sequenceDiagram
    participant V as Viewer
    participant L as lakeFS
    L->>V: Load viewer page
    V->>L: viewer-ready { metadataOnly? }
    L->>V: object-response { content, contentType, size, path }
    opt Read a range
        V->>L: range-request { id?, start, rangeSize }
        alt Read succeeds
            L->>V: range-response { id, content, start }
        else Read fails
            L->>V: range-error { id, start, error }
        end
    end
    opt Set minimum height
        V->>L: set-iframe-height { height }
    end

응답은 요청과 다른 순서로 도착할 수 있어요. viewer-ready 직후 range 요청을 보낸 뷰어는 object-response보다 range-response를 먼저 받을 수 있죠. range 응답은 id로 요청과 맞춰요.

뷰어가 보내는 메시지

viewer-ready
{
  type: "viewer-ready";
  metadataOnly?: boolean; // default false
}

메시지 리스너를 등록한 뒤 이 메시지를 한 번만 보내요. lakeFS는 object-response로 객체 전체를 담아 답해요. 크기 제한은 없고, 콘텐츠는 메모리에 버퍼링돼요.

metadataOnly: true를 설정하면 다운로드를 건너뛰고 range 요청으로 필요한 바이트만 읽어요. Reading part of a large object 섹션의 예시를 보세요.

range-request
{
  type: "range-request";
  id?: unknown;      // echoed in the reply; use a unique value
  start: number;     // 0 or greater, below the object size
  rangeSize: number; // 1 or greater
}

start 오프셋부터 rangeSize 바이트를 읽어요. lakeFS는 range-response로 답하고, 읽기가 실패하면 range-error로 답해요. 잘못된 값의 요청에는 응답이 없어요.

동시에 최대 하나의 요청만 걸려 있을 때만 id를 생략할 수 있어요. 이때 응답의 id는 undefined예요.

set-iframe-height
{
  type: "set-iframe-height";
  height: string; // CSS length, such as "600px"
}

프레임의 최소 높이를 설정해요. 콘텐츠 높이가 바뀔 때마다 다시 보내세요.

lakeFS가 보내는 메시지

object-response
{
  type: "object-response";
  content: ArrayBuffer | null; // the whole object; null when metadataOnly was set
  contentType: string;         // content type from the object's metadata
  size: number;                // full object size in bytes
  path: string;                // object path in the repository
}

viewer-ready에 대한 응답으로 보내져요. content는 뷰어가 metadataOnly를 요청했을 때만 null이에요. lakeFS가 객체를 로드하지 못하면 UI에 오류를 보고하고 응답을 보내지 않아요.

range-response
{
  type: "range-response";
  id: unknown;          // id from the matching request
  content: ArrayBuffer; // bytes read from the object
  start: number;        // start from the matching request
}

성공한 range-request에 대한 응답으로 보내져요. 유효한 range가 객체 끝을 넘으면 content에는 남은 바이트만 담겨요. start + content.byteLength가 object-response의 size와 같으면 끝에 도달한 거예요.

range-error
{
  type: "range-error";
  id: unknown;   // id from the matching request
  start: number; // start from the matching request
  error: string; // description of the failure
}

요청한 range를 가져오는 데 실패했을 때 보내져요.

더 알아보기 (Learn more)

공식 문서: lakeFS Custom Object Viewer 가이드