Composite action으로 Custom Action 만들기

Created at 2025년 10월 07일

Updated at 2025년 10월 07일

By 강병준

tl;dr

GitHub Actions의 복잡한 workflow를 재사용 가능한 Composite Action으로 만들고, TypeScript로 로직을 구현하며, act로 로컬 테스트하고, Marketplace에 배포하는 전체 과정을 다룹니다.

개요

현대 소프트웨어 개발에서 많은 개발자들이 반복적인 작업을 자동화하기 위해 Github Actions를 활용한 자동화 테스트, 배포 등 다양한 CI/CD 파이프라인을 구축하고 있습니다.

하지만 workflow 파일 (.yml) 내부에서 직접 스크립트를 작성하다 보면 몇 가지 한계에 부딪히게 됩니다. 복잡한 로직을 YAML 파일 안에서 bash나 actions/github-script 등으로 작성하다 보면 가독성이 떨어지게 되고 설령 동작하는 워크플로우를 모두 만들었다 하더라도, 이후 시간이 지남에 따라 유지보수가 점점 어려워집니다. 나중에 수정이 필요할 때 어디를 고쳐야 할지 찾기도 힘들고, 실수로 다른 부분을 건드릴 위험도 있습니다.

더 큰 문제는 재사용성입니다. 동일 repository 라면 Reuse workflows를 활용할 수 있겠지만, 만약 여러 repository에서 동일한 자동화 로직을 사용해야 한다면 어떻게 해야 할까요? 각 repository마다 같은 workflow 코드를 복사-붙여넣기 하는 것은 비효율적일 뿐만 아니라, 나중에 로직을 수정할 때 모든 repository를 일일이 찾아다니며 업데이트해야 하는 악몽 같은 상황이 발생합니다.

바로 이런 문제들을 해결할 수 있는 것이 Composite Action입니다. Composite Action을 사용하면 복잡한 로직을 독립적인 Action으로 분리하여 관리할 수 있고, 필요한 입력값(inputs)만 전달하여 쉽게 사용할 수 있습니다. 이번 글에서는 나만의 Composite Action을 만들고, 이를 활용하는 방법을 알아보겠습니다.

Composite Action

Composite Action은 여러 개의 workflow step들을 하나의 재사용 가능한 Action으로 묶어주는 방식입니다. 쉽게 말해, 자주 사용하는 step들의 조합을 "함수"처럼 만들어두고 필요할 때마다 다음과 같이 호출할 수 있게 해줍니다.

# action.yml
name: 'Hello World'
description: 'Greet someone'
inputs:
  who-to-greet:
    description: 'Who to greet'
    required: true
    default: 'World'
outputs:
  random-number:
    description: "Random number"
    value: ${{ steps.random-number-generator.outputs.random-number }}
runs:
  using: "composite"
  steps:
    - name: Set Greeting
      run: echo "Hello $INPUT_WHO_TO_GREET."
      shell: bash
      env:
        INPUT_WHO_TO_GREET: ${{ inputs.who-to-greet }}
 
    - name: Random Number Generator
      id: random-number-generator
      run: echo "random-number=$(echo $RANDOM)" >> $GITHUB_OUTPUT
      shell: bash

이렇게 정의한 Action은 다른 repository의 workflow에서 다음과 같이 간편하게 사용할 수 있습니다.

# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: your-username/hello-world-action@v1
        with:
          who-to-greet: 'Github Actions'

Composite Action의 주요 특징은 다음과 같습니다.

  • 간단한 작성: YAML 문법만으로 작성 가능
  • 입출력 정의: inputs와 outputs로 명확한 인터페이스 제공
  • 재사용성: 여러 repository에서 동일한 로직을 재사용

물론 이렇게 step들을 하나의 action.yml에 정의하는 것은 간편하지만, 복잡한 로직을 작성해야 하는 경우라면 가독성이 여전히 저하되는 문제가 있습니다. 예를 들어 Github API를 호출하여 issue를 필터링하고, 조건에 따라 라벨을 추가하고, 코멘트를 달아야 한다면 bash 스크립트만으로는 한계가 있습니다.

이를 해결하기 위해 Github Actions는 여러 타입의 Custom Action을 지원하며, 특히 JavaScript Action과 Composite Action을 함께 사용하면 복잡한 로직을 깔끔하게 처리할 수 있습니다.

타입설명장점단점
Composite Action여러 step을 YAML로 정의간단하고 빠르게 작성 가능, 다른 Action들을 조합 가능복잡한 로직, 작성 시 가독성 저하
JavaScript ActionNode.js로 작성빠른 실행 속도, 복잡한 로직 구현 가능JavaScript/TypeScript 지식 필요
Docker ActionDocker 컨테이너로 실행모든 언어 사용 가능, 완전한 환경 제어느린 실행 속도, 이미지 빌드 필요

JavaScript Action을 함께 사용하기

위 방식들 중 저는 JavaScript Action과 Composite Action을 함께 사용하는 방식을 선택했습니다. JavaScript에 익숙하기도 하고, Docker Action보다 빠르면서도 다른 Action들과 조합할 수 있는 유연성을 확보하고 싶었기 때문입니다.

예시를 통해 빠르게 확인해보겠습니다. 프로젝트 구조는 다음과 같습니다:

my-action/
├── action.yml          # Composite Action 정의
├── dist/
│   └── index.js        # 빌드된 JavaScript 파일
├── src/
│   └── index.ts        # TypeScript 소스 파일
├── package.json
└── tsconfig.json

src/index.ts에는 실제 복잡한 로직을 작성합니다:

// src/index.ts
import * as core from '@actions/core';
import * as github from '@actions/github';
 
async function run() {
  try {
    const token = process.env.INPUT_GITHUB_TOKEN as string;
    const daysBeforeStale = parseInt(process.env.INPUT_DAYS_BEFORE_STALE as string);
 
    const octokit = github.getOctokit(token);
 
    // 복잡한 로직을 TypeScript로 깔끔하게 작성
    const issues = await octokit.rest.issues.listForRepo({
      owner: github.context.repo.owner,
      repo: github.context.repo.repo,
      state: 'open'
    });
 
    // ... 더 많은 로직
 
    core.info('Cleanup completed!');
  } catch (error: any) {
    core.setFailed(error.message);
  }
}
 
run();

action.yml에서는 JavaScript 파일을 실행하도록 정의합니다.

# action.yml
name: 'Issue Cleanup'
description: 'Close stale issues automatically'
inputs:
  github-token:
    description: 'Github token'
    required: true
  days-before-stale:
    description: 'Days before marking as stale'
    required: true
    default: '30'
 
outputs:
  result:
    description: 'close stale result message'
 
runs:
  using: "composite"
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
 
    - name: Run cleanup script
      run: node ${{ github.action_path }}/dist/index.js
      shell: bash
      env:
        INPUT_GITHUB_TOKEN: ${{ inputs.github-token }}
        INPUT_DAYS_BEFORE_STALE: ${{ inputs.days-before-stale }}

여기서 ${{ github.action_path }}는 현재 실행 중인 Action의 루트 디렉토리 경로를 가리키며, 빌드된 파일을 기준으로 호출하기 때문에 index.js로 설정하면 실행할 준비가 완료됩니다.

동작 원리

이렇게 편리한 Composite Action이 실제로 어떻게 동작하는지 Workflow(Caller)에서 Action(Callee)을 호출하면 어떤 과정을 거쳐 실행되고 결과가 반환되는지 시퀀스 다이어그램으로 살펴보겠습니다.

image.png

위 다이어그램에서 볼 수 있듯이, trigger되어 Workflow가 실행되면 Composite Action은 다음과 같은 순서로 동작합니다.

  1. Action 호출 및 초기화
    • Workflow(Caller)에서 uses로 Composite Action을 호출하고 with로 필요한 inputs 전달
    • Action(Callee)은 action.yml 파일을 파싱하여 inputs/outputs, composite 확인
  2. Steps 순차 실행
    • action.yml의 runs.steps에 정의된 각 step들이 GitHub Runner에서 실행
    • 전달받은 inputs는 INPUT_* 형식의 환경변수로 변환되어 각 step에서 사용
    • 실행 결과나 생성된 값은 GITHUB_OUTPUT 파일에 key=value 형식으로 저장
  3. 결과 반환 및 활용
    • 모든 step이 완료되면 Action은 정의된 outputs를 수집하여 Caller에게 반환
    • Workflow에서는 steps.{action-step-id}.outputs.{output-name} 형식으로 접근

이러한 구조 덕분에 복잡한 로직을 action.yml에 캡슐화하고, Workflow에서는 간단한 인터페이스(inputs/outputs)만으로 사용할 수 있게 됩니다.

디버깅과 테스트

Action을 만들었으니 이제 제대로 동작하는지 확인해야 합니다. 하지만 매번 Github에 push하고 workflow를 실행해보는 것은 비효율적이므로 디버깅하는 방법과 로컬에서 테스트하는 방법을 알아보겠습니다.

console로 디버깅하기

가장 기본적인 디버깅 방법은 로그를 출력하는 것입니다. Github Actions는 @actions/core 패키지를 통해 다양한 로그 레벨을 지원합니다.

// src/index.ts
import * as core from '@actions/core';
 
async function run() {
  try {
    // 일반 정보 출력
    core.info('Starting issue cleanup...');
 
    // 디버그 로그 (ACTIONS_STEP_DEBUG secret을 true로 설정해야 보임)
    core.debug('Processing JavaScript Action');
 
    // 경고 메시지
    core.warning('No issues found to clean up');
 
    // 에러 메시지 (workflow는 계속 실행됨)
    core.error('Failed to process issue #123');
 
    // workflow 실패 처리
    core.setFailed('Critical error occurred');
 
    // 그룹화로 로그 정리
    core.startGroup('Checking stale issues');
    core.info('Issue #1: 30 days old');
    core.info('Issue #2: 45 days old');
    core.endGroup();
 
  } catch (error: any) {
    core.setFailed(error.message);
  }
}
 
run();

위 코드를 실행하면 Github Actions 로그에서 다음과 같은 결과를 확인할 수 있습니다:

image.png

core.debug()를 확인하기 위해서는 repository의 Settings → Secrets and variables → Actions에서 아래의 secret을 추가하시면 됩니다.

  • Name: ACTIONS_STEP_DEBUG
  • Value: true

act로 테스트하기

Action을 만든 repository 내부에서 테스트 workflow를 만들어 GitHub에 push하고 결과를 확인하는 방법도 있지만, act를 사용하여 로컬 환경에서 Github Actions를 실행할 수 있습니다.

먼저 act 공식 문서에서 설치를 완료한 이후 앞서 작성한 로깅 테스트 코드를 호출하는 테스트 workflow를 만들어보겠습니다. act를 실행하기 전 act Installation을 참고하세요.

# .github/workflows/logging-test.yml
name: Test Logging and Debugging
on:
  workflow_dispatch:
 
jobs:
  console:
	  runs-on: ubuntu-latest
	
		steps:
		  - name: Checkout
		    uses: actions/checkout@v4
		
		  - name: Run JavaScript
		    uses: ./

⚠️ 주의: 테스트를 할 때는 workflow에 uses: ./를 사용해야 한다는 점입니다. uses: bangdori/test-logging-actions@main은 이미 GitHub에 배포된 버전을 테스트하는 거라 로컬 테스트의 의미가 없어질 수 있습니다.

이제 터미널에서 다음 명령어로 로컬에서 테스트할 수 있습니다.

# console job 실행
act -j console

위 명령어를 실행하면 다음과 같이 로컬에서 Action이 실행되는 것을 확인할 수 있습니다:

image.png

배포

Action을 테스트까지 완료했다면 이제 다른 사람들이 사용할 수 있도록 배포하고 버전을 관리할 차례입니다. 배포 방법은 사용 범위에 따라 크게 두 가지로 나뉩니다:

  1. Organization에서 사용하는 경우
  2. Github Marketplace에 공개하는 경우

기본 배포

action.yml을 작성하고 초기 세팅을 마친 상태라면 별도의 추가 작업 없이도 사용할 수 있습니다:

  • 개인 repository: 본인만 사용 가능
  • Organization repository: Organization 멤버 누구나 사용 가능
# .github/workflows/example.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # Organization의 private repository에서도 사용 가능
      - uses: org/custom-action@1.0.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

⚠️ 주의: uses에서는 버전 태그가 아닌 브랜치 이름(@main)을 사용할 수 있는데, 이는 항상 최신 코드가 실행되므로 예상치 못한 변경사항이 적용될 수 있습니다. 그렇기 때문에 안정적인 사용을 위해서는 버전 태그를 사용하는 것이 좋습니다.

Github Marketplace에 출시하기

Action을 외부에 공개하여 더 많은 개발자들과 공유하기 위해서는 Marketplace에 등록하는 과정을 거쳐야합니다. 이를 위해서는 action.ym에 아래 정보를 추가해야 합니다.

1. action.yml에 메타데이터 추가

name: 'Action name'
description: 'Action description'
author: 'Your Name'
 
# Marketplace 브랜딩
branding:
  icon: 'trash-2'  # Feather 아이콘
  color: 'red'
 
inputs:
  # ... 기존 inputs
 
runs:
  # ... 기존 runs

branding icon은 github에서 제공하는 github-action-branding을 참고하세요! 좋은 아이콘이 많이 있습니다!

2. Release 생성

image.png

마지막 단계로 action.yml 파일을 정의한 repository 상단에 위와 같이 표시됩니다. Draft a release를 클릭하여 기본 정보(태그, 제목, 설명)를 입력해줍니다.

  • Publish this Action to the GitHub Marketplace

그리고 마지막으로 위 체크 박스를 체크하고 Publish release를 클릭하면 출시가 완료됩니다!

끝으로

이번 글에서는 GitHub Actions의 재사용성과 유지보수성 문제를 해결하기 위한 Composite Action 작성법을 살펴보았습니다. 핵심 내용을 정리하면:

  1. Composite Action: 여러 step을 하나로 묶어 재사용 가능한 Action 생성
  2. JavaScript Action 조합: TypeScript로 복잡한 로직을 깔끔하게 구현
  3. 로컬 테스트: act를 활용한 빠른 개발 사이클 구축
  4. 배포: Organization 내부 사용부터 Marketplace 공개까지

특히 JavaScript Action과 Composite Action을 함께 사용하면, YAML의 간결함과 TypeScript의 표현력을 동시에 활용할 수 있어 복잡한 자동화 로직도 체계적으로 관리할 수 있습니다.

(깨알 홍보)

과거부터 내부적으로 사용되는 다양한 워크플로우를 만들면서 GitHub Marketplace에 직접 Action을 출시해보고 싶다는 목표가 있었습니다. 혼자라면 쉽지 않았을 일이지만, Claude Sonnet 4.5의 도움을 받아 빠르게 목표를 달성할 수 있었습니다. 🎉

Agentic 시대가 오면서 문서 정리나 작성이 정말 쉬워졌고, 이로 인해 SDD 등의 방법론이 유행하면서 크고 작은 프로젝트에서 AI에 MCP 서버를 연동해 PRD 문서나 Task들을 GitHub 이슈로 관리하는 경우가 많아졌습니다. 이번에 제가 만든 GitHub Action은 이러한 이슈들을 자동으로 추적하고 정리(Closed)해주는 도구입니다.

아직 초기 버전이라 기본 기능만 구현되어 있지만, 관심 있으신 분들은 사용해보시고 피드백 주시면 감사하겠습니다!

긴 글 읽어주셔서 감사합니다.

참고