GitHub App 제대로 이해하기: OAuth App, PAT와 무엇이 다를까

자동화를 만들다 보면 GitHub에 인증할 일이 계속 생깁니다. CI에서 릴리스 커밋을 올리고, 봇이 이슈에 댓글을 달고, 외부 서비스가 PR을 검사하는 식이죠. 가장 먼저 손이 가는 건 개인 액세스 토큰(Personal Access Token, 이하 PAT)인데요. 잘 돌아가다가도 꼭 한 번씩 벽에 부딪힙니다. “이 봇이 왜 내 계정으로 커밋하지?”, “내가 퇴사하면 이 자동화도 멈추나?”, “왜 이 토큰에 우리 조직 전체 권한이 다 붙어 있지?” 같은 의문이죠. 🤔
이 문제들의 답이 바로 GitHub App입니다. 이름만 보면 거창한 애플리케이션 같지만, 본질은 “사람과 분리된 독립적인 자동화 주체”예요. 이번 글에서는 GitHub App이 PAT나 OAuth App과 무엇이 다른지, 그리고 개인 키에서 시작해 API를 호출하기까지의 인증 흐름을 차근차근 살펴보겠습니다.
GitHub App은 사람이 아니라 봇입니다
핵심부터 짚으면, GitHub App은 사람 계정에 묶이지 않는 독립된 신원입니다. PAT는 내 계정의 권한을 그대로 빌려 쓰는 토큰이라, 내가 떠나거나 권한이 바뀌면 자동화도 같이 흔들립니다. 토큰을 만든 사람이 곧 행위의 주체인 셈이죠.
GitHub App은 다릅니다. 앱 자체가 하나의 행위자라서, 저장소에 “설치(install)“되면 그 설치 단위로 권한을 받고 독립적으로 행동합니다. 그래서 App이 남긴 커밋이나 댓글은 my-app[bot]처럼 봇 계정으로 표시돼요. 누가 만들었든 상관없이 “이건 그 자동화가 한 일”이라는 게 분명해집니다.
이 차이가 실무에서 꽤 크게 와닿습니다. 봇이 한 작업과 사람이 한 작업이 깔끔하게 구분되고, 특정 개인에게 의존하지 않으니 담당자가 바뀌어도 자동화가 멈추지 않거든요.
OAuth App과는 또 다릅니다
여기서 한 번 더 헷갈리는 지점이 있습니다. 바로 OAuth App인데요. 이름이 비슷해서 같은 걸로 착각하기 쉽지만, 둘은 역할이 완전히 다릅니다.
OAuth App은 “사용자를 대신해서” 행동합니다. 사용자가 로그인하고 권한을 승인하면, 그 사용자의 자격으로 GitHub에 접근하는 거죠. 그래서 OAuth App이 한 일은 결국 그 사용자가 한 일로 기록됩니다. 흔히 보는 “GitHub로 로그인” 버튼이 OAuth App의 대표적인 쓰임이에요. 예를 들어 Better Auth에서 GitHub 소셜 로그인을 붙일 때 넣는 GITHUB_CLIENT_ID와 GITHUB_CLIENT_SECRET은 GitHub OAuth App에서 발급받는 자격 증명입니다.
반면 GitHub App은 “자기 자신으로서” 행동합니다. 사용자를 대리하는 게 아니라 독립 주체로 움직이죠. 셋을 나란히 두면 이렇게 갈립니다.
- PAT: 내 개인 계정의 권한을 빌려 쓰는 토큰. 사람에 묶이고, classic 토큰은 권한이 광범위합니다.
- OAuth App: 사용자를 대신해 행동. 로그인과 권한 위임에 적합하고, 행위는 그 사용자로 기록됩니다.
- GitHub App: 독립된 봇 주체. 저장소 단위로 설치되고, 세밀한 권한과 짧은 수명 토큰으로 자동화에 적합합니다.
표로 비교하면 차이가 더 또렷합니다.
| 구분 | PAT | OAuth App | GitHub App |
|---|---|---|---|
| 행위 주체 | 내 계정(사람) | 사용자 대리 | 봇 또는 사용자 |
| 저장소 접근 | 내 계정 전체 | 사용자 접근 범위 전체 | 설치된 저장소만 |
| 권한 | 광범위(classic) | 광범위 스코프 | 세밀(읽기/쓰기 분리) |
| 토큰 수명 | 길거나 무기한 | 무기한(취소 전까지) | 짧음(1시간) |
| 설치 | 불필요 | 불필요 | 필요 |
예전에는 “봇 작업은 GitHub App, 사용자 로그인은 OAuth App”으로 역할이 갈렸습니다. 그런데 지금은 그 경계가 거의 사라졌어요. GitHub App 하나로 봇 작업과 로그인을 모두 처리할 수 있거든요. 어떻게 로그인까지 되는지, 그렇다면 OAuth App은 대체 언제 쓰는지 차례로 살펴보겠습니다.
로그인도 GitHub App으로 됩니다
여기가 가장 많이들 헷갈리는 부분이에요. GitHub App은 두 종류의 토큰을 발급합니다. 지금까지 이야기한 설치 액세스 토큰은 앱이 “봇으로서” 저장소에 작업할 때 쓰는 server-to-server 토큰이에요. 여기에 더해, 사용자를 대신해 행동하는 사용자 액세스 토큰(user access token)도 발급할 수 있는데, 이게 바로 “GitHub로 로그인”을 구현하는 열쇠입니다.
사용자 로그인 흐름은 OAuth App과 똑같은 web application flow를 따릅니다. 사용자를 GitHub 인가 페이지로 보내고, 승인하면 콜백으로 일회용 code가 돌아오고, 그 code를 사용자 액세스 토큰으로 교환한 뒤, 토큰으로 /user를 불러 누가 로그인했는지 확인하는 순서예요.
curl -X POST https://github.com/login/oauth/access_token \
-H "Accept: application/json" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "code=${CODE}"
여기서 핵심은 OAuth App과 똑같은 엔드포인트(/login/oauth/authorize, /login/oauth/access_token)를 쓴다는 점이에요. 그래서 OAuth 2.0에서 본 인가 코드(Authorization Code) 흐름과 구조가 사실상 같습니다. 다만 GitHub App 쪽이 더 안전한데, 사용자 액세스 토큰에 만료와 갱신 토큰(refresh token)을 켤 수 있기 때문이에요. OAuth App 토큰이 사용자가 직접 취소하기 전까지 무기한 유효한 것과 대조적이죠.
엔드포인트가 같다 보니 Better Auth 같은 제네릭 OAuth 클라이언트도 GitHub App에서 발급한 client_id와 client_secret을 그대로 넣으면 로그인이 붙습니다. 다만 OAuth App에서 옮겨올 때 실제로 걸려 넘어지는 지점이 두 군데 있어요.
첫째, GitHub App은 OAuth 스코프를 무시합니다. OAuth App에서는 read:user user:email처럼 스코프로 접근 범위를 정했는데, GitHub App은 이걸 권한(permission)으로 지정해요. 그래서 로그인한 사용자의 이메일을 받으려면 앱 설정에서 계정 권한(Account permissions)의 Email addresses를 읽기로 켜야 합니다. 스코프 문자열만 그대로 복사했다가 이메일이 안 넘어와서 한참 헤매기 딱 좋은 함정이죠.
둘째, 콜백 URL을 여러 개 넣을 수 있습니다. OAuth App은 콜백이 하나뿐이라 프로덕션용과 로컬 개발용으로 앱을 두 개 따로 만들어야 했는데, GitHub App은 콜백을 최대 10개까지 등록할 수 있어요. https://myapp.com/callback과 http://localhost:5173/callback을 한 앱에 담으면 환경마다 client_id가 갈리지 않아서, 로컬과 프로덕션이 같은 자격 증명을 공유하고 관리할 앱도 하나로 줄어듭니다.
참고로 두 앱은 client_id 접두사로도 구별됩니다. OAuth App은 Ov23으로, GitHub App은 Iv23(구형은 Iv1.)으로 시작해요. 로그인이 안 될 때 어느 쪽 자격 증명을 넣었는지 헷갈린다면 접두사만 봐도 금방 확인할 수 있습니다.
그래서 OAuth App은 언제 쓸까
봇 작업도, 로그인도 GitHub App으로 된다면 OAuth App은 쓸 일이 없을까요? 거의 그렇습니다. GitHub 공식 문서도 “대체로 GitHub App이 OAuth App보다 낫다”고 분명히 안내해요. 그래도 OAuth App이 더 단순한 경우가 딱 하나 남아 있습니다.
바로 설치 과정 없이, 사용자가 접근할 수 있는 모든 저장소에 한 번에 접근해야 할 때예요. GitHub App은 설치가 전제라, 사용자나 조직이 “어느 저장소에 쓸지” 고른 곳에만 접근합니다. 반면 OAuth App은 설치 개념이 없어서, 사용자가 한 번 승인하면 그 사용자가 접근 가능한 모든 저장소를 다룰 수 있어요.
그래서 계정을 통째로 백업하거나, 다른 곳으로 마이그레이션하거나, 모든 저장소를 분석하는 CLI처럼 사용자의 전체 저장소를 훑는 도구라면 OAuth App이 편합니다. GitHub App으로 같은 일을 하려면 저장소나 조직마다 일일이 설치해야 하니까요.
다만 이 편리함은 보안 비용과 맞바꾼 겁니다. 광범위한 스코프(repo 하나면 읽기, 쓰기, 설정이 전부 열려요)에 만료 없는 토큰이라, 토큰이 새면 피해가 크고 오래갑니다. 뒤집어 보면 GitHub App의 “설치 단위 제어”와 “짧은 토큰”은 불편함이 아니라 안전장치인 셈이죠. 그러니 사용자 전체 접근이 꼭 필요한 게 아니라면, 새 통합은 GitHub App으로 시작하는 게 정석입니다.
인증은 세 단계로 흐릅니다
GitHub App의 인증이 처음엔 복잡해 보이는데, “개인 키 → JWT → 설치 토큰”이라는 세 단계로 이해하면 명쾌합니다. 왜 단계가 나뉘는지부터 보면 외우지 않아도 됩니다.
앱을 만들면 GitHub이 App ID와 개인 키(private key)를 줍니다. 이 개인 키로 “나는 이 앱이 맞다”를 증명하는 게 1단계예요. JWT를 만들어 서명하는데, 대칭 키가 아니라 RSA 개인 키로 서명하는 RS256 알고리즘을 씁니다. GitHub은 앱의 공개 키로 검증할 수 있고요.
import jwt from "jsonwebtoken";
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
iat: now - 60, // 시계 오차 대비 60초 과거로
exp: now + 600, // 만료는 최대 10분
iss: CLIENT_ID, // 앱의 Client ID (또는 App ID)
},
PRIVATE_KEY,
{ algorithm: "RS256" },
);
여기서 exp가 최대 10분이라는 게 중요한데요. 이 JWT는 GitHub에 “내가 그 앱이다”라고 말하는 용도일 뿐, 실제 저장소 작업에는 쓰지 않습니다. 그래서 수명을 짧게 잡아도 충분하고, 오히려 유출 위험을 줄여줘요.
2단계는 이 JWT로 설치 액세스 토큰(installation access token)을 발급받는 겁니다. 앱이 설치된 곳마다 고유한 installation_id가 있는데, 거기에 대고 토큰을 요청해요.
curl -X POST \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens
이렇게 받은 설치 토큰은 1시간 뒤 만료됩니다. 그리고 이 토큰이 비로소 실제 저장소 작업에 쓰이는 3단계 열쇠예요.
curl -X POST \
-H "Authorization: Bearer ${INSTALLATION_TOKEN}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/owner/repo/issues \
-d '{"title":"봇이 만든 이슈"}'
흐름을 다시 정리하면, 만료가 긴 개인 키는 내 손 안에만 두고, 그걸로 짧은 JWT(10분)를 만들어 신원을 증명한 뒤, 실제 작업용으로는 더 짧은 설치 토큰(1시간)을 그때그때 발급받아 씁니다. 비밀이 한 겹씩 짧아지면서 노출 범위가 좁아지는 구조죠. GitHub REST API를 호출할 때 이 설치 토큰을 Authorization 헤더에 실으면 됩니다.
매번 토큰을 직접 만들 필요는 없습니다
세 단계를 일일이 손으로 짜는 건 번거롭죠. 다행히 공식 SDK인 Octokit이 이 과정을 알아서 처리해줍니다. App ID와 개인 키만 넘기면 JWT 생성과 설치 토큰 발급, 만료 시 갱신까지 내부에서 다 해줘요.
import { App } from "octokit";
const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY });
// 설치 ID로 인증된 클라이언트를 얻으면 토큰 관리는 Octokit이 담당
const octokit = await app.getInstallationOctokit(INSTALLATION_ID);
await octokit.rest.issues.create({
owner: "owner",
repo: "repo",
title: "봇이 만든 이슈",
});
설치하려면 bun add octokit (또는 npm install octokit)이면 됩니다. 직접 JWT를 굽는 코드를 짜는 대신, 인증 메커니즘이 어떻게 돌아가는지만 이해하고 실무에서는 이런 도구에 맡기는 게 안전하고 깔끔해요.
권한은 잘게 쪼개서 줍니다
GitHub App이 자동화에 특히 잘 맞는 이유가 권한 모델입니다. classic PAT는 repo 스코프 하나만 줘도 사실상 모든 저장소에 대한 광범위한 접근이 열립니다. 반면 GitHub App은 필요한 권한만 골라서 줄 수 있어요.
권한은 대상별로 잘게 나뉩니다. 예를 들어 저장소 권한만 봐도 콘텐츠(contents), 이슈(issues), 풀 리퀘스트(pull_requests), 배포(deployments)처럼 항목이 갈리고, 각각을 읽기(read) 또는 쓰기(write)로 따로 지정합니다. “이슈에 댓글만 다는 봇”이라면 이슈 쓰기 권한 하나만 주면 되는 거죠. 최소 권한 원칙을 지키기가 훨씬 쉬워집니다.
게다가 설치 토큰을 발급받을 때 범위를 한 번 더 좁힐 수도 있어요. 특정 저장소와 특정 권한만 담은 토큰을 그때그때 만드는 겁니다.
curl -X POST \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens \
-d '{
"repository_ids": [321],
"permissions": { "issues": "write" }
}'
이러면 앱 전체에 이슈와 콘텐츠 권한이 있더라도, 이 토큰만큼은 321번 저장소의 이슈 쓰기로 한정됩니다. 혹시 토큰이 새더라도 피해 범위가 그 안에 묶이는 거죠. 권한을 세밀하게 통제하는 방법은 GitHub Actions 권한 글에서 다룬 원칙과도 통합니다.
한 가지 덤으로, GitHub App은 rate limit도 유리합니다. 설치된 저장소 수에 비례해 한도가 올라가서, 조직 단위 자동화에서 PAT보다 훨씬 여유 있게 API를 쓸 수 있어요.
GitHub Actions에서 GitHub App 토큰 쓰기
실무에서 GitHub App을 가장 자주 만나는 곳이 GitHub Actions입니다. 워크플로의 기본 토큰인 GITHUB_TOKEN은 그 저장소 안으로 권한이 제한되고, 이걸로 푸시한 커밋은 다른 워크플로를 다시 트리거하지 못한다는 제약이 있어요. 그래서 “릴리스 커밋이 다시 CI를 돌려야 하는” 상황에선 GitHub App 토큰을 씁니다.
공식 액션을 쓰면 앞서 본 세 단계가 한 스텝으로 끝납니다.
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.app-token.outputs.token }}
create-github-app-token이 App ID와 개인 키로 설치 토큰을 만들어 steps.app-token.outputs.token으로 내보내고, 이후 스텝들이 그 토큰을 가져다 씁니다. 개인 키는 당연히 코드가 아니라 시크릿(secrets.APP_PRIVATE_KEY)으로 넣어야 하고요. 이렇게 발급한 토큰으로 커밋하면 작성자가 봇 계정으로 표시되고, GITHUB_TOKEN과 달리 후속 워크플로도 정상적으로 트리거됩니다. 시크릿과 토큰을 안전하게 다루는 더 자세한 원칙은 GitHub Actions 보안에서 정리했습니다.
마치며
GitHub App은 “사람과 분리된 자동화 주체”라는 한 가지 아이디어에서 출발합니다. 그 덕분에 봇이 한 일과 사람이 한 일이 구분되고, 특정 개인에게 묶이지 않으며, 권한을 잘게 쪼개 최소한만 줄 수 있어요. 인증은 개인 키로 JWT를 만들어 신원을 증명하고(10분), 그걸로 설치 토큰을 받아(1시간) 실제 작업에 쓰는 세 단계 흐름이고, 실무에서는 Octokit이나 공식 액션이 이 과정을 대신해줍니다.
헷갈릴 때를 위해 한 줄로 정리하면 이렇습니다. 봇 자동화든 사용자 로그인이든 새로 시작한다면 GitHub App이 정답이고, OAuth App은 “설치 없이 사용자의 모든 저장소에 접근”해야 하는 특수한 경우에만 고려하면 됩니다. PAT는 빠른 스크립트나 개인용으로는 여전히 편하지만, 팀의 자동화라면 봇 계정으로 분리되는 GitHub App이 낫고요.
토큰 인증의 기반이 되는 JWT의 구조나, OAuth 자체의 다양한 grant type이 궁금하다면 관련 글을 먼저 보셔도 좋습니다.
더 자세한 내용은 GitHub Apps 공식 문서를 참고하세요.
This work is licensed under CC BY 4.0