Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

GitHub의 저장소 기능을 확장할 수 있는 Blocks

GitHub Next는 새로운 기술을 연구하고 프로토타이핑하는 연구조직 같은 팀이다. 여기서 새로운 아이디어를 구현해서 테스트하고 피드백 받으면서 잘 다듬어지면 제품으로 만들기 때문에 GitHub Next의 사이트를 보면 프로젝트마다 제품이 되었는지 아직 프로토타이핑 단계인지 등이 표시되어 있고 GitHub Copilot도 GitHub Next의 프로젝트 중 하나였다.

GitHub Blocks는 현재 제한된 사람만 사용할 수 있는 테크니컬 프리뷰 상태라 신청해 놓고 허용되어야 사용할 수 있다. 몇 주 전에 Blocks 접근 허용을 받아서 사용을 해봤다. 아래 설명에서도 blocks.githubnext.com 주소들이 나오는데 홈페이지 외에는 권한을 받아야만 접근할 수 있다.

GitHub Blocks 홈페이지

저장소에 있는 각 파일의 추가 기능을 제공해서 목적에 맞게 더 풍부한 기능을 제공할 수 있게 하는 기능이다. 홈페이지에 나온 "저장소를 다시 상상해 봐라"라는 말대로 사람들이 Blocks를 만들어서 공유하고 이를 저장소에 사용하면 저장소의 기능을 확장할 수 있다. 예를 들어 저장소에 올린 코드 외에 이미지 파일이나 SVG 파일을 GitHub에서 열어보면 텍스트 파일로 보이는 게 아니라 이미지가 그대로 보이거나 SVG를 렌더링해서 볼 수 있게 된다. 이는 GitHub에서 해당 파일에 대해서 기능을 제공하는 것인데 이런 기능을 사람들이 직접 만들 수 있다고 생각하면 된다. 직접 보면 이해하기 쉽다.

Blocks 살펴보기

GitHub Blocks에서 시작하기를 누르고 로그인하면 githubnext/blocks 저장소의 githubnext.com 페이지로 이동하게 된다.

GitHub Blocks 저장소

코드 위에 보면 기존 저장소에는 없던 Block이라는 메뉴가 보이는데 이를 눌러보면 사용할 수 있는 Block 목록을 볼 수 있다.

사용할 수 있는 GitHub Blocks 목록

선택한 Blocks의 기능에 따라 코드가 나오는 화면에 Blocks가 적용된 화면이 나오게 된다. 마크다운 프리뷰는 항상 보던 화면이므로 githubnext/blocks에서 제공하는 예제를 살펴보자. 스크린샷 만으로는 동작을 확인하기 어려울 것 같아서 (용량은 좀 크지만) 영상으로 만들어 봤다.

GitHub Blocks 데모

지금은 테스트고 실제로 서비스에 적용되게 된다면 정확히 어떤 모습일지 더 궁금하긴 하지만 Blocks로 어떤 일을 할 수 있는지 어느 정도 이해했겠다고 생각한다. 범용적으로도 확장할 부분이 많지만 회사내에서는 정해진 형식을 사용할 가능성이 더 높기 때문에 이런 부분의 Blocks를 만들어서 GitHub의 활용도를 높일 수 있을 걸로 보인다. 다만 Blocks를 선택하는 건 좀 귀찮긴 하다.

Blocks 만들기

Blocks는 직접 만들 수 있고 나중에는 마켓플레이스가 열려서 자신이 만든 Blocks를 올려서 공유할 수 있게 될 것으로 보인다. 아직 문서가 충실하진 않지만 githubnext/blocks에서 개발 문서를 제공하고 있다.

Blocks 개발에 참고할 수 있게 템플릿 저장소예제 저장소를 제공하고 있고 Blocks 개발을 쉽게 할 수 있는 모듈이 포함된 blocks-dev가 있다.

Blocks를 만들려면 먼저 템플릿 저장소를 포크해서 저장소를 만든 다음에 로컬에 클론 받는다. Blocks 저장소에 github-blocks 태그를 지정하도록 권장하고 있다.

yarn으로 의존성을 설치하고 yarn start로 개발 서버를 로컬에서 실행한다.

$ yarn
$ yarn start
Starting the development server at http://localhost:4000

개발 환경은 흥미롭게 구성되어 있는데 http://localhost:4000에 접근하면 https://blocks.githubnext.com/?devServer=http%3A%2F%2Flocalhost%3A4000%2F로 이동해서 개발할 수 있게 한다.

처음 살펴볼 때처럼 Let's get started 버튼을 누르면 아까 데모를 blocks.githubnext.com에서 확인한 것처럼 똑같이 이동하지만 https://blocks.githubnext.com/githubnext/blocks/blob/main/README.md?devServer=http%3A%2F%2Flocalhost%3A4000%2F처럼 로컬 개발 서버와 연결하는 devServer 쿼리스트링가 붙어있게 된다.

하지만 githubnext/blocks 저장소는 GitHub Next의 저장소이므로 수정 권한이 없어서 Blocks 개발을 제대로 할 수 없다. 앞에서 템플릿 저장소를 포크하면서 만든 자신의 저장소로 이동해야 한다. 예를 들어 나처럼 저장소 주소가 github.com/outsideris/blocks-demo라면 blocks.githubnext.com/outsideris/blocks-demo에 접속해야 푸시도 하고 수정도 해보면서 테스트할 수 있다. 그리고 여기서는 로컬 개발 서버와 연결해서 테스트해야 하므로 https://blocks.githubnext.com/outsideris/blocks-demo?devServer=http%3A%2F%2Flocalhost%3A4000%2F처럼 쿼리스트링에 devServer를 붙여준다.

개발서버와 연결된 Blocks

이 저장소에서 Block 메뉴를 열어보면 "Example File Block"이라는 내 저장소의 Blocks를 볼 수 있다.(템플릿에 미리 만들어져 있던 Blocks다.) 이는 devServer 쿼리스트링으로 로컬에 실행한 개발 서버와 연결되어 로컬에서 작성된 Blocks를 불러온 것이다. 이를 통해 blocks.githubnext.com에서 테스트해보면서 Blocks를 개발할 수 있게 된다.

Blocks는 정의파일과 Blocks 코드로 구성되어 있다.

blocks.config.json

저장소 루트 디렉터리에 blocks.config.json 파일로 Blocks를 정의한다. 템플릿에 포함되어 있던 blocks.config.json는 다음과 같이 생겼다.

[
  {
    "type": "file",
    "id": "file-block",
    "title": "Example File Block",
    "description": "A basic file block",
    "entry": "blocks/example-file-block/index.tsx",
    "matches": ["*"],
    "example_path": "https://github.com/facebook/react/blob/main/packages/react-dom/index.js"
  },
  {
    "type": "folder",
    "id": "folder-block",
    "title": "Example Folder Block",
    "description": "A basic folder block",
    "entry": "blocks/example-folder-block.tsx",
    "matches": ["*"],
    "example_path": "https://github.com/githubocto/flat"
  }
]

아까 봤던 Example File BlockExample Folder Block 2개의 Blocks가 정의된 것을 알 수 있다. 이 Blocks 정의의 형식은 다음과 같다.

interface BlockDefinition {
  type: "file" | "folder";
  id: string;
  title: string;
  description: string;
  entry: string;
  matches?: string[];
}
  • type: 이 Blocks를 파일에 적용할지 폴더에 적용할지를 지정
  • id: Blocks의 식별자 문자열로 프로젝트 내에서 유일해야 한다.
  • title: 표시할 Block 이름
  • description: 표시할 Block 설명
  • entry: Blocks의 진입점 파일을 프로젝트 루트에서 상대 경로로 지정한다.
  • matches: 해당 Blocks가 적용되어야 할 파일을 globs 배열로 지정한다. ["*.json"]는 JSON 파일에만 적용되고 ["*"]는 모든 파일에 적용된다.
  • example_path: 이름으로 보면 예제 링크를 지정하는 거 같은데 문서에 없어서 어디 표시되는지는 모르겠다.

Blocks 코드

코드는 TypeScript로 작성하고 blocks/ 디렉터리에 위치한다.

폴더를 선택하고 Block에서 Example Folder Block을 선택하면 다음과 같이 해당 폴더 안에 있는 각 파일의 내용이 정리된 것을 볼 수 있다.

예제 폴더 Blocks

이 Blocks의 코드는 blocks/example-folder-block.tsx에 있다.

import { FolderBlockProps } from "@githubnext/blocks";
import { Box } from "@primer/react";

export default function ExampleFolderBlock(props: FolderBlockProps) {
  return (
    <Box p={4}>
      <Box
        borderColor="border.default"
        borderWidth={1}
        borderStyle="solid"
        borderRadius={6}
        overflow="hidden"
      >
        <Box
          bg="canvas.subtle"
          p={3}
          borderBottomWidth={1}
          borderBottomStyle="solid"
          borderColor="border.default"
        >
          This is the folder content.
        </Box>
        <Box p={4}>
          <table style={{ textAlign: "left" }}>
            <thead>
              <tr>
                <th className="p-1">Path</th>
                <th className="p-1">Size</th>
                <th className="p-1">Type</th>
              </tr>
            </thead>
            <tbody>
              {props.tree.map((item, index) => (
                <tr key={index}>
                  <td className="p-1">{item.path}</td>
                  <td className="p-1">{item.size}</td>
                  <td className="p-1">{item.type}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </Box>
      </Box>
    </Box>
  );
}

React에 익숙하다면 어렵지 않게 이해할 수 있는 코드이다. 폴더 타입의 Blocks이므로 FolderBlockProps를 받아서 이 정보를 이용해서 화면을 그려주는 코드이고 반드시 export default로 Block 컴포넌트를 노출해야 한다.

이번엔 좀 더 복잡한 Example File Block을 살펴보자. 해당 파일 경로와 파일의 언어를 보여주고 아래는 파일의 내용을 보여준다. 중간에는 메타데이터 예시가 있다.

예제 파일 Blocks

이 Blocks는 blocks/example-file-block/example-folder-block.tsx에 있다.

import { FileBlockProps, getLanguageFromFilename } from "@githubnext/blocks";
import { Button, Box } from "@primer/react";
import "./index.css";

export default function ExampleFileBlock(props: FileBlockProps) {
  const { context, content, metadata, onUpdateMetadata } = props;
  const language = Boolean(context.path)
    ? getLanguageFromFilename(context.path)
    : "N/A";

  return (
    <Box p={4}>
      <Box
        borderColor="border.default"
        borderWidth={1}
        borderStyle="solid"
        borderRadius={6}
        overflow="hidden"
      >
        <Box
          bg="canvas.subtle"
          p={3}
          borderBottomWidth={1}
          borderBottomStyle="solid"
          borderColor="border.default"
        >
          File: {context.path} {language}
        </Box>
        <Box p={4}>
          <p>Metadata example: this button has been clicked:</p>
          <Button
            onClick={() =>
              onUpdateMetadata({ number: (metadata.number || 0) + 1 })
            }
          >
            {metadata.number || 0} times
          </Button>
          <pre className="mt-3 p-3">{content}</pre>
        </Box>
      </Box>
    </Box>
  );
}

@githubnext/blocks가 Blocks에 쓰기 편한 기능을 제공해 주므로 getLanguageFromFilename로 파일에서 언어를 추출했다. FileBlockProps에서 파일의 내용 등을 가져와서 아까처럼 페이지를 그려주었다.

메타데이터 관리

Example File Block에는 메타데이터라는 부분이 있었다. Blocks는 기능을 제공하는데 상황에 따라서는 상태를 저장해 두어야 할 수 있으므로 메타데이터는 Blocks를 사용하는 곳에서 상태를 저장할 수 있게 하는 기능이 나온다. 메타데이터 업데이트는 onUpdateMetadata()를 사용하는데 앞의 코드를 보면 버튼을 클릭할 때 number의 값을 1씩 증가시켰다.

onUpdateMetadata({ number: (metadata.number || 0) + 1 })

앞에서 보았던 "0 times"라는 버튼을 클릭하면 아래와 같이 커밋 화면이 나온다.

메타데이터 커밋 모달

그리고 이제 01로 바뀐걸 볼 수 있다.

업데이트된 메타데이터가 표시된 버튼

위에 커밋 화면이 나온 것처럼 Blocks를 사용하는 저장소에서 .github/blocks/ 안에 저장되는데 블록마다 메타데이터는 따로 관리되며 폴더/파일 단위로 저장된다. 여기서는 blocks/example-file-block/index.css 파일에 대해서 outsideris 계정의 blocks-demo 저장소의 file-block Blocks의 메타데이터이므로 .github/blocks/file/outsideris__blocks-demo__file-block/blocks%2Fexample-file-block%2Findex.css.json라는 파일이 생기고 여기에서 JSON으로 데이터가 관리된다.


GitHub Blocks는 GitHub에서 기능을 제공해 주지 않아도 직접 커스텀한 기능을 추가할 수 있다는 면에서 매력적으로 느껴진다. 마켓플레이스가 열리면 많은 사람이 좋은 기능을 만들어 줄 것 같다.

2023/03/05 17:40 2023/03/05 17:40