미끼를 물었다

대장❤️ : 옆팀에서 마크업 저장소들을 폐쇄망으로 옮겨야 한다는데..
폐쇄망에 있는 git은 html 페이지를 바로 확인할 수 있는 url이 없다네요.
디자인 검수 때문에 필요하다고 해서.. 폐쇄망 내부에 따로 서버를 두고 ftp로 관리해야되나 고민중이예요.

(주: git enterprise를 사용중이다.)

나🤔 : git pages 쓰면 안되나요?

대장❤️ : 브랜치별로도 구분해서 확인해야 하는데, git pages는 지정한 브랜치만 된다고 해서요.
그리고 저장소가 많아서 일일이 세팅하는 것도 일이라고 하네요.

나🤔 : 음.. 그러면 미리보기용 저장소를 따로 하나 만들어서 거기다가 ftp처럼 파일을 올리면 어떨까요?
거기만 git pages 설정하면 될 것 같은데.. 서버 관리할 사람 따로 안 둬도 되고..

대장❤️ : 음.. 그것도 좋은 방법 같네요..
근데 그렇게 하면 매번 파일들을 미리보기 폴더로 복사해서 commit 한 다음 push 해야 되는거 아닌가요?

나🤔 : 복사하고 commit하고 push하는 과정을 스크립트로 짜두면, 명령어 한줄로 처리할 수 있지 않을까요?

대장❤️ : 그럼 스크립트 좀 개발해주시겠어요?

미리보기 모듈

목표

Git pages를 이용해서 프로젝트 브랜치의 미리보기 URL을 생성하는 모듈 개발하기

조건

폐쇄망에다가 담당자도 여러명이고 적용할 저장소가 많은 상황이었다.
세팅과 사용이 번거로우면 의미가 없을 것 같아서 Zero-Configuration을 목표로 했다.

  • 개별 프로젝트에서 세팅 최소화
  • 외부 모듈을 사용하지 않고 nodejs 기본 모듈만 사용
  • 명령어 한줄로 실행
  • 프로젝트명/브랜치명 폴더 구조 자동 생성

사용 방식

프로젝트 저장소마다 미리보기 세팅 과정을 생략하려면 어떻게 해야 될까 고민하다가..
미리보기 모듈을 별도의 폴더에서 관리하고, 개별 프로젝트 폴더에서 미리보기 폴더로 접근해서 스크립트를 실행하도록 했다.

Project
   ├─ preview/index.js // 미리보기 모듈과 미리보기 파일들을 관리
   ├─ project-a
   └─ project-b

따라서 미리보기 저장소를 한번만 클론하면 project-aproject-b폴더에서 별도의 설정 없이 아래와 같이 실행 할 수 있다.

$ node ../preview

필요한 정보

구현에 필요한 값들을 별도로 설정하거나, 실행할 때 파라미터로 전달해야 되지 않도록 직접 알아낼 필요가 있다.

예를 들어 project-a 폴더에서 node ../preview를 실행했을 때를 가정하면 아래와 같이 파악할 수 있다.

실제 모듈은 사내 git enterprise 환경 기준으로 개발되었으나 본 글은 github 기준으로 작성하였다.

미리보기 폴더 경로

미리보기 모듈(index.js)이 있는 경로이므로 __dirname를 이용하면 알 수 있다.

// ***/preview
const previewRoot = __dirname;

프로젝트 폴더 경로

node를 실행한 경로이므로 path.resolve를 파라미터 없이 호출하면 알 수 있다.

const path = require('path');

// ***/project-a
const projectRoot = path.resolve();

프로젝트명

앞에서 파악한 projectRoot에서 /(ios) 혹은 \(windows) 앞 부분을 모두 삭제해서 폴더명만 남긴다.

// project-a
const projectName = projectRoot.replace(/^.*[\/\\]/, '');

브랜치명

git branch --show-current을 이용하면 현재 브랜치명을 알 수 있다.

execSync로 실행해 반환된 내용을 참조한다.
node를 실행하는 위치가 기준이 되므로 project-a의 브랜치명이 반환된다.

const { execSync } = require('child_process');

// fix/#123
const branchName = execSync('git branch --show-current').toString().trim();

브랜치 폴더명

브랜치명의 특수문자는 url에서 사용 가능한 형태로 변경한다.
decodeURIComponent를 사용할 경우 #이나 /가 처리되지 않는다.

// fix/#123 -> fix-123
const branchDirName = branchName.replace(/[^a-z0-9\-_]+/gi, '-');

유저명, 저장소명

git remote -v를 이용해 미리보기 저장소의 {유저 or 조직명}/{저장소명} 부분을 추출한다.

execSynccwd를 설정하면 node를 실행한 경로가 아니라 cwd에 설정한 경로 기준으로 실행된다.
미리보기 저장소의 정보가 필요하므로 cwd에 미리보기 폴더 경로인 previewRoot를 설정한다.

// user-id/preview
const previewRepoPath = execSync('git remote -v', {cwd: previewRoot})
    .toString()
    .replace(/^[\s\S]*origin\s+.+\/([^\/]+\/[^\/]+)\.git[\s\S]*$/, '$1');

// user-id, preview
const [user, repo] = previewRepoPath.split('/');

Pages URL

앞에서 추출한 정보에서 userrepo 정보를 확인해 pages URL 경로를 만든다.

// https://user-id.github.io/preview
const previewRootURL = `https://${user}.github.io/${repo}`;

Github의 경우는 git pages를 세팅하는 저장소명이 {유저명}.github.io{유저명}.github.io URL의 root 경로를 사용할 수 있고, 다른 이름의 저장소는 {유저명}.github.io/{저장소명} 형태로 접근할 수 있다.

Github의 pages는 아래와 같은 제약이 있다.

  • 저장소 용량 1G
  • 월 트래픽 100G
  • 시간당 10번 이내 빌드

https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages#usage-limits

구현

재료 준비가 되었으니 이제 구현해보자.
본 글에서는 전반적인 기능 구현에 대한 흐름만 설명하고 세부 구현에 대한 내용은 생략한다.
생각나는 짤이 있긴 한데.. 너무 뻔해서 사용하지 않았다.

미리보기 폴더 동기화

미리보기 폴더를 reset 하고, 원격 저장소에 업데이트 된 내용이 있으면 반영한다.

const gitPull = [
  'git reset --hard HEAD',
  'git pull'
].join('&&');

execSync(gitPull, {cwd: previewRoot});

git 명령이나 파일 복사 과정에서 정상적으로 처리되지 않는 경우가 있을 수 있다.
이 때 잘못된 내용이 push 되지 않게 진행을 중단하는 프로세스를 넣어주는게 좋다.

try {
  const result = execSync(gitPull, {cwd: previewRoot}).toString();
  console.log(result);
} catch(error) {
  // 오류일 때 처리
  console.log(error.stdout.toString());
}

폴더 생성과 파일 복사

먼저 미리보기 경로에 프로젝트 폴더가 없으면 만들어준다.
프로젝트 폴더에서 미리보기 브랜치 폴더로 파일을 복사해준다.
(주: createDir, deleteDir, copyDir은 직접 구현한 함수)

// 미리보기 프로젝트 폴더: ***/preview/project-a
const projectDir = path.join(previewRoot, projectName);

// 미리보기 브랜치 폴더: ***/preview/project-a/fix-123
const branchDir = path.join(projectDir, branchDirName);

// 미리보기 폴더에 프로젝트 폴더가 없으면 만든다.
createDir(projectDir);

// 프로젝트 폴더에 브랜치 폴더가 있으면 기존 내용은 삭제
deleteDir(branchDir); 

// 대상 폴더를 미리보기 브랜치 폴더로 복사
// 개발 환경 관련, 시스템 파일 등은 ignore 처리하거나 혹은 산출물 폴더만..
copyDir(projectRoot, branchDir);

저장소 반영

// commit log는 브랜치 폴더명 대신 원래 브랜치명을 사용
// [publish] project-a (fix/#123)
const gitPush = [
  `git add ./${projectName}/${branchDirName}`,
  `git commit -m "[publish] ${projectName} (${branchName})"`,
  'git push'
].join('&&');

execSync(gitPush, {cwd: previewRoot});

저장소 관리를 위해 node ../preview remove, node ../preview clear 형태로 파라미터를 추가해서 브랜치 폴더 삭제나 프로젝트 폴더 삭제 처리도 구현했다.

미리보기 URL 안내

저장소에 push 하고나면, 최종적으로 pages URL에 프로젝트명과 브랜치 폴더명을 조합해 미리보기 URL을 생성하고 터미널에 출력한다.

// project-a의 fix/#123 브랜치 미리보기 경로
const previewURL = path.join(previewRootURL, projectName, branchDirName);

// https://user-id.github.io/preview/project-a/fix-123
console.log(`>>> URL: ${previewURL}`);

에피소드

현재 프로젝트의 미리보기 브랜치 폴더 전체를 삭제하는 기능(node ../preview clear)을 만들어 테스트했다.
삭제를 처리하는 코드에서 미리보기의 프로젝트 경로(projectDir)가 아닌 실제 프로젝트 경로(projectRoot)를 사용하는 실수를 저질렀다.
기능을 실행했더니 테스트하던 원본 프로젝트 폴더 내용이 삭제 되었다.
.git 폴더도 같이 삭제되어 reflog로 복구가 불가능해 로컬 커밋을 몇 개 날렸다.
이후 삭제 함수에서 projectRoot, previewRoot 폴더는 삭제할 수 없도록 예외처리했다.

마치며

정리해놓고 보니 별거 아니긴 하지만..
여러 프로젝트에서 설정 없이 사용하기 위해서 Zero-Configuration을 목표로 하다보니 개발 초기에 생각해야될 부분이 많았다.

어쨌든 이런 방식으로 자동화 스크립트 같은걸 구현해서 따로 모아서 관리하면, 필요한 곳에서 별도의 설정 없이도 쉽게 사용할 수 있을 것 같다. 예를 들면 readme에서 cdn이나 demo URL 같은 곳에서 사용된 구버전 정보를 찾아서 package.json에 입력된 신버전으로 교체하는 처리같은..

profile
What 12 9oing on?

0개의 댓글