제가 활동하고 있는 글 쓰는 개발자 커뮤니티, 글또에 참여하는 모든 사람들은 2주마다 각자의 기술 블로그에 포스트를 작성하고 이를 제출해야 합니다.
그런데 지난 기수(9기)부터는 조금 특별한 피드백 시스템이 도입되었습니다. 바로 포스트를 제출하게 되면, 별안간 글빼미 라는 이름의 슬랙 봇이 나타나 제출된 글을 검수하고 피드백을 남겨준다는 것이죠.
글빼미는 글 링크를 제출하면 해당 페이지에 대한 정량적 피드백을 계산해서 알려준다
글빼미는 제출한 글의 스크린샷을 비롯해 몇 가지 정량적 지표를 측정하고 그 결과를 글 제출자와 운영진에게 요약해 제공합니다. 즉, 글빼미를 한 마디로 표현하자면 포스트 검수를 자동화하는 봇 이라고 설명할 수 있겠네요.
사실 글빼미의 개발과 운영은 제가 담당하고 있습니다. 최근에 글또 10기가 시작되면서 글빼미가 다시 활약을 하게 되었는데요, 글또 10기에는 650여 명의 참여자가 존재하는 만큼 글빼미의 역할이 더 중요해질 것으로 예상됩니다.
그래서 이번 글에서는 글빼미를 먼저 소개하고, 이를 만들게 된 이유와 기술적 동작 원리, 그리고 이를 통해 얻은 효과 등을 소개해 보는 시간을 가져보려고 합니다. 이번 글을 통해 E2E 테스트 자동화와 슬랙 앱의 연동 사례에 대해 궁금하신 분들 께 도움이 되었으면 좋겠습니다.
제작 동기
글빼미라는 이름은 글또 + 올빼미 의 합성어입니다. 올빼미가 큰 눈을 이용해 밤에 먹이를 사냥하듯, 글빼미는 글또에서 제출된 글을 검수하여 어뷰징을 방지한다는 의미를 담고 있죠. 그런데 왜 글또에서 글을 검수하는 봇이 필요했을까요?
앞서 말했듯 글또 참여자는 2주마다 각자 블로그에 포스트를 작성하고 이를 제출해야 합니다. 강제성을 부여하기 위해 보증금을 맡겨야 하고, 글을 제출하지 못하면 보증금이 차감되는 규칙이 있습니다. 제출된 글은 당연히 전체 공개가 되어 있어야 하고, 코드와 공백을 제외한 글자 수가 1000 자 이상이어야 합니다.
이는 글또의 가이드 문서 에 명시되어 있는 요구사항입니다. 이런 조건을 걸어둔 것은 글또의 목적 중 하나가 바로 개발자의 글쓰기 역량 강화를 목적으로 하는 커뮤니티이기 때문입니다.
많은 참여자들이 초반에는 열정을 갖고 시작하지만, 시간이 흐를수록 의욕이 떨어지고 현생에 치여 글을 목표한 대로 쓰지 못하는 경우가 생기곤 합니다. 그런데 사람이 워낙 많다 보니, 보증금이 깎이는 것이 싫어서 어뷰징을 하는 참여자도 심심찮게 존재합니다.
지금까지 글또에서 발견할 수 있었던 대표적인 어뷰징 케이스는 다음과 같습니다.
- 글을 정말 성의 없이 작성하여 제출하는 경우
- 글을 미완성한 채로 제출한 후, 마감 기한 이후에 수정하여 완성하는 경우
- 1000자라는 글자수 제한을 코드 블럭으로 채우는 경우
- 글은 잘 작성되었지만, 접근 권한을 외부에 허용하지 않아 비공개 상태인 경우
이런 사례가 실제로 발생했을 때 공정하고 적절한 조치가 이루어지지 않는다면, 다른 참여자들의 의욕을 꺾을 뿐만 아니라 글또 시스템에 대한 불신까지 이어지는 결과가 일어날 수도 있겠죠.
사실 글또의 인원이 적었을 때에는 운영진이 직접 모든 글을 수동으로 검수하는 것이 가능했습니다. 그런데 글또가 진행될수록 참여자의 수가 늘어났고 10기에는 650여 명에 달하는 참여자가 존재하게 되었습니다. 이 인원들이 6개월간 각자 블로그에 포스트를 작성한다면 최대 7800개에 달하는 글이 작성될 텐데… 이를 운영진이 수동으로 검수하는 것은 불가능해졌습니다.
그래서 저는 이런 어뷰징을 기술적으로 탐지할 수 있는 방법을 연구하게 됐고, 이를 글빼미를 통해 구현하게 되었습니다.
파이프라인
사용자는 글빼미가 남긴 슬랙 메시지만 확인할 수 있기 때문에 글빼미가 마치 하나의 단일 애플리케이션처럼 보일 수 있지만, 글빼미는 사실 여러 단계의 파이프라인으로 이루어져 있습니다. 이 파이프라인을 간단히 설명하자면 다음과 같습니다.
- 글빼미는 평일은 24시간 주기로, 주말은 3시간 주기로 동작하는 배치(batch) 작업
- 검수할 기간 내에 제출된 글 링크와 유저 정보를 수집하고 정규화
- 제출된 글 개수만큼 E2E 테스트를 수행하여 스크린샷 촬영 및 지표 측정
- 테스트 결과를 운영진 채널과 글 제출자에게 슬랙 메시지로 전송
즉, 글빼미를 기술적으로 좀 더 정확하게 설명하자면 배치 방식으로 E2E 테스트를 수행하여 그 결과를 슬랙 API를 이용해 전송하는 봇 이라고 할 수 있습니다.
한편, 글빼미를 구현하기 위해 사용한 기술 스택은 다음과 같습니다.
- TypeScript
- Playwright
- Google Sheets API
- GitHub Actions
- Slack API
그렇다면 이제 각 파이프라인 단계를 좀 더 자세히 살펴보겠습니다.
GitHub Actions를 이용한 배치
글빼미는 글이 제출된 즉시 검수를 실행하는 게 아니라, GitHub Actions를 이용해 배치로 작업을 실행합니다. 이는 부하를 줄이기 위해서인데요.
글또에서는 2주 주기로 글을 제출하는데, 제출 마감일 자정에 가까워지면 거의 분당 수십 건의 글이 제출되곤 합니다. 이런 상황에서 글빼미가 글이 제출되는 즉시 테스트를 실행하는 방식으로 구현하면, 동시에 실행되는 워크플로우가 너무 많아지게 될 것입니다. 워크플로우가 초기화될 때, Node.js 설치나 디펜던시 설치 같은 무거운 작업을 반복하는 것은 매우 비효율적이기 때문입니다.
따라서 특정 주기마다 작업을 실행하는 방식으로 구현했는데요, 이때의 배치 주기는 평일은 24시간, 주말은 3시간으로 설정했습니다. 아무래도 주말에는 글이 제출되는 횟수가 많아서, 사용자에게 더 잦은 주기로 피드백을 주는 것이 더 낫다는 경험적인 부분에 근거해 조정하였습니다.
다만 이 작업을 할 때 좀 번거로웠던 점은 GitHub Actions는 UTC 기준으로 동작하므로 이를 위한 타임존 계산이 별도로 필요했고, 주말에만 주기를 줄이기 위한 별도 로직 계산이 필요했다는 것… 정도가 있었습니다.
데이터 수집과 정규화
글또의 참여자와 제출된 글에 대한 모든 데이터는 구글 스프레드시트에 저장됩니다. 따라서 배치에 의해 실행된 워크플로우는 이 스프레드시트의 데이터를 읽어와서 적절히 정규화하여야 합니다.
우선 특정 URL을 가진 스프레드시트에 접근하여 데이터를 읽을 수 있어야 하는데, 이 때는 google-spreadsheet 라는 라이브러리를 사용했습니다. 저도 처음 써보는 라이브러리였는데, 꽤 간단하고 편리해서 사용 경험이 꽤 괜찮았습니다.
아무튼 이 라이브러리를 이용하면 스프레드시트의 특정 시트, 열과 행 데이터를 쉽게 읽을 수 있는데요. 이를 통해 제출된 글 목록을 수집하고, 이를 유저 정보와 매핑하여 JSON 형태로 정규화했습니다.
[
{
"round": "1회차",
"team": "3_프론트_f",
"koName": "정종윤",
"title": "글빼미야! 블로그 검수 자동화를 부탁해",
"contentUrl": "https://wormwlrm.github.io/2024/10/12/Geultto-Owl-Project.html",
"dt": "2024-10-11 00:20:03",
"ts": "1728573603.697109"
},
{
// ...
}
]
이 JSON은 node.js API를 이용해서 현재 워크플로우가 실행되는 액션 러너의 로컬 디스크에 임시로 저장을 합니다. 이렇게 저장하는 이유는 파이프라인의 각 작업을 분리하고, 이전 작업의 결과물을 다음 작업에 전달할 수 있도록 하기 위해서입니다.
E2E 테스트 실행
이렇게 해서 정규화된 JSON 데이터가 로컬 디스크에 저장된 상태라면, 이제 이 데이터를 기반으로 한 본격적인 E2E 테스트를 실행할 수 있습니다.
E2E 테스트가 무엇인지 모르는 분들을 위해 간단히 설명드리자면, E2E 테스트(End-to-End Test) 는 사용자의 관점에서 소프트웨어의 전체적인 흐름을 검증하는 테스트로, 실제 사용자의 입장에서 상호작용하는 테스트 코드를 작성하여 애플리케이션을 테스트하는 방법입니다.
일반적으로 프론트엔드 개발에서 일컫는 E2E 테스트란, 터미널 환경에서 헤드리스 브라우저를 이용해 CI/CD 시 자동으로 테스트를 실행하는 것을 말하며 이를 위한 유명한 라이브러리로는 Puppeteer, Cypress, Playwright 등이 있습니다.
저는 Playwright를 선택했는데요, 회사 업무에서 사용해 본 경험도 있고 오픈 소스인데다가 문서도 꽤 잘 되어 있었던 것이 이유였습니다. 사실 E2E 테스트 라이브러리 중에서는 가장 핫한 녀석이기도 하죠.
다시 E2E 테스트 생성을 위한 JSON 이야기로 돌아가봅시다. 아까 우리가 정규화한 JSON 데이터는 배열 형태로 되어 있었는데요, 이를 for
문을 이용하면 반복문을 통해 테스트 케이스를 여러 개 생성할 수 있습니다.
for (const line of data) {
const { team, koName, title, contentUrl } = line;
// 테스트 케이스를 반복문으로 생성
test(`[${team}] ${koName} - ${title}`, async ({ page }) => {
// ...
});
}
자, 이제 개별 테스트 케이스에 대해 상세한 시나리오를 작성해 볼까요. 각 테스트 케이스에서 가장 먼저 해야 할 일은 바로 제출한 글의 링크로 이동하는 것입니다. 이는 Playwright의 page.goto
메서드를 이용하면 간단히 할 수 있습니다.
await page.goto(contentUrl);
위 코드는 대부분의 경우에 잘 동작하지만, 노션을 비롯한 일부 웹사이트에서는 올바르게 동작하지 않는 경우가 있었습니다. 이런 경우를 위해 URL의 도메인 패턴을 분석하여 본문이 나타나는 페이지까지 정확하게 이동하도록 예외 처리를 해주어야만 했습니다.
이렇게 해서 본문 페이지에 도달하면 본격적으로 지표 측정을 시작합니다. 그런데 문제가 있습니다. E2E 테스트 코드를 실행할 웹 페이지는 제가 통제하는 것이 아니라 외부의 웹 페이지이기 때문에, 링크 별 페이지의 HTML 구조가 모두 다를뿐더러 그 구조가 언제든지 바뀔 수 있다는 점입니다. 즉 마크업 구조와 특정 CSS 선택자를 이용해 본문만 정확히 추출하는 것은 사실상 불가능합니다.
따라서 이를 위해서는 사람의 직관이 들어간 몇 가지 지표를 이용해 보조 지표로 활용하는데요, 이를 코드 스멜에 빗대어 어뷰징 스멜이라고 불러보겠습니다.
글자 수 계산
HTML을 읽어온다고 해도 이런 잡다한 영역들이 존재하기 때문에 정확히 본문만 추출하는 것은 어렵다
우선 가장 기본적인 지표인 본문 글자 수를 세어보도록 하겠습니다.
특정 마크업에서 글자를 추출하는 것은 Playwright의 locator.textContent()
메서드를 이용하면 쉽게 가능합니다. 다만 모든 HTML 태그에 대해서 locator.textContent()
를 실행하면 본문과 상관없는 텍스트들도 함께 포함되기 때문에 정확한 글자수 계산에 오류가 생깁니다. 그러므로 글자 수 계산에 포함하지 않을 글자는 일반적으로 다음과 같은 방법을 이용해 제외합니다.
- 마크업 상 본문에 해당할 수 있는 태그(
p
,li
,h1
등)을 미리 정의하고 이 태그들에 해당하는 내용만 추출locator.textContent()
로 추출한 텍스트가 완전히 동일한 경우는 일반적인 문장 구조에서는 나올 수 없으므로 1회만 계산- CSS의
display: none;
처리 등으로 인해 사용자가 직접 눈으로 볼 수 없는 글자는 본문에 해당하지 않을 것이므로 제외- 공백과 줄 바꿈 문자도 글자 수 계산에서 제외
- 블로그 플랫폼의 종류에 따라 위 로직을 적용했음에도 불구하고 글자 수 계산에 포함되는 텍스트가 있을 수 있으므로 플랫폼 별로 최소 글자수 분기
위 로직을 적용했을 때에는 대부분의 경우에 실제 본문 글자 수와 비슷한 값을 얻을 수 있었지만, 노션은 마크업 구조가 워낙 독특하게 되어 있어서 예외 처리를 추가로 해주어야만 했습니다. 노션 마크업 파싱이 사람을 아주 열받게 만들더라구요. 😇
코드 블럭을 제외한 블로그 본문의 높이
코드 블럭을 제외한 본문의 높이가 너무 짧은 경우는 분량이 적다는 것을 의미하므로 어뷰징 스멜로 의심할 수 있다
두 번째는 보조 지표로, 코드 블럭을 제외한 블로그 본문의 높이를 측정하는 것입니다.
이러한 지표를 측정하고자 하게 된 이유는, 글자 수가 적은 포스트는 브라우저에서 세로 스크롤할 높이가 상대적으로 짧을 것이라는 어뷰징 스멜 때문이었는데요. 따라서 다음과 같은 방법을 이용해 블로그 본문의 높이를 측정했습니다.
- Playwright의
page.evaluate
메서드를 이용해<html>
,<body>
와 같은 최상위 태그의scrollHeight
,offsetHeight
를 측정
하지만 본문 높이가 길더라도, 본문 대부분이 코드 블럭으로 채워진 경우는 어뷰징 스멜에 해당하지만 높이 탐지만으로는 이를 걸러내기 어렵기 때문에 코드 블럭에 해당하는 태그의 높이는 제외하는 방식으로 값을 보정했습니다.
- 쿼리 셀렉터를 이용해 본문 내 모든 코드 블럭의 높이를 제외하고 블로그 본문의 높이를 계산
블로그 본문의 높이 대비 코드 블럭의 비율
전체 높이 중에서 코드 블럭의 비율이 지나치게 높은 경우도 단순 코드를 복붙한 경우일 수 있으므로 어뷰징 스멜로 의심할 수 있다
세 번째도 보조 지표로, 블로그 본문의 높이 대비 코드 블럭의 전체 비율을 살펴보는 것입니다.
두 번째 지표와 비슷한데, 이번에는 본문 높이를 기준으로 코드 블럭이 차지하는 비율이 지나치게 높다면 이는 코드 만으로 채워진 어뷰징 스멜에 해당할 것이라고 판단했습니다.
- 블로그 본문 전체 높이를 기준을 코드 블럭의 비율이 75% 이상인 경우 어뷰징 스멜로 판단
스크린샷 촬영
이렇게 정량적인 지표를 측정했다면, 사용자가 제출한 글의 전체 스크린샷을 촬영합니다. 이는 테스트 코드 실행 당시의 스크린샷을 스냅샷으로 보관해 두어, 혹시라도 생길 수 있는 판정 시비나 논란을 방지하기 위함입니다. 마치 자동차의 블랙박스처럼요.
또한 혹시라도 게시글의 권한이 비공개로 설정되어 있거나, 링크를 잘 못 올렸거나, 블로그 플랫폼의 서버 문제로 인해 글 조회가 제대로 않는 경우를 미리 탐지하여 대책을 논의할 수 있습니다.
await page.screenshot({
path: `screenshots/${round}/${koName}.jpeg`,
fullPage: true,
// 용량을 줄이기 위해 품질은 0으로 설정
quality: 0,
});
이렇게 촬영된 스크린샷은 현재 워크플로우가 실행되는 액션 러너의 로컬 디스크에 저장됩니다.
테스트 결과 보고
이렇게 해서 모든 지표 수집과 스크린샷 촬영이 끝나면, 결과물을 글 제출자와 운영진에게 슬랙 메시지로 전송해야 합니다. 이미지 파일을 슬랙 메시지에 첨부하여 보내기 위해서는 단순히 메시지 전송이 아니라 파일 업로드 API를 이용해야 합니다.
await slack.files.uploadV2({
thread_ts: message?.ts,
channel_id: message?.channel,
filename: `${round}-${koName}.jpeg`,
initial_comment: getComment(),
});
이렇게 하면 아래 사진처럼 사진을 첨부한 슬랙 메시지를 보낼 수 있는데요.
글자 수는 실제 판정 기준이 되기 때문에 사용자에게 전송하지만, 보조 지표는 단순히 어뷰징 스멜을 탐지하기 위한 추가적인 정보일 뿐이기 때문에 사용자에게는 전송하지 않습니다.
운영진에게는 모든 테스트 결과물을 한 곳에서 확인할 수 있게 별도의 채널을 생성하고 결과 메시지를 보낸다
반면 운영진 채널에는 모든 지표를 참고하여 어뷰징 여부를 정확히 탐지할 수 있어야 하기 때문에 보조 지표도 함께 전송합니다. 위 사진처럼 모든 테스트 결과를 하나의 공간에서 받아볼 수 있도록 채널을 별도로 생성했고, 여기에 결과 메시지를 보내도록 했습니다.
만약 어뷰징 스멜이 탐지된 경우, 운영진은 해당 글을 다시 직접 확인하여 글의 최소 기준을 만족할 수 있도록 가이드를 제공하는 식으로 대응하게 됩니다.
효과와 한계
이렇게 해서 지금까지 글빼미를 구현하게 된 동기와 동작 원리에 대해 알아보았습니다. 마지막으로 이를 통해 얻은 효과와 한계에 대해 얘기해 볼텐데요.
글빼미는 사실 8기 때부터 운영진 내부에서만 사용하려는 용도로 제가 조금씩 구현해 오고 있었고, 9기에 이르러서야 본격적으로 사용자에게 결과물을 제공하게 되었습니다.
글빼미를 도입한 이후 가장 큰 효과는 아무래도 글또 운영진이 어뷰징 검수에 소요되는 시간을 대폭 줄일 수 있었다는 것입니다. 이전에는 글을 제출하면 운영진이 수동으로 모든 글을 확인하고, 애매한 글을 판단하여 올바른 글인지 아닌지 피드백을 주는 과정을 거쳤는데요. 아무래도 반복적이고 지루한 작업이었기 때문에 상당한 리소스가 소요되었습니다. 하지만 이를 글빼미를 통해 자동화하면서 운영진의 업무 효율을 높일 수 있었습니다.
사용자의 반응도 대체로 긍정적이었는데요, 사실 글빼미는 어뷰징 탐지의 목적이 가장 크긴 하지만 자동화된 봇에 의해 내 글이 분석된다는 점이 사용자들에게는 꽤나 흥미로운 경험을 제공했던 것 같습니다. 글빼미가 얼른 와서 코멘트 남겨주면 좋겠다고 말씀하신 분도 계셨으니까요.
물론 본문 파싱 로직의 한계로 인해 초반에는 오차도 많고 오류가 많았지만, 여러 가지 개선 과정을 통해 오차를 줄여나가서 현재는 어느 정도 믿고 쓸만한 수준이 되었습니다.
다만 아직까지 최종 결정은 운영진이 직접 내려야 하기 때문에 100% 완전한 자동화는 아니라는 점이 한계이지만, 글빼미가 어뷰징 스멜이 나는 글을 탐지할 수 있게 한 것만으로도 충분히 효과적이라고 생각합니다. 추후에는 사용자 피드백에 LLM 을 이용한 분석 결과를 추가하여 좀 더 사용자에게 유용한 정보를 제공할 수 있을 것이라고 기대하고 있습니다.
마무리
후후… 그 시스템… 제가 만든 겁니다
개인적으로 사이드 프로젝트 만드는 걸 즐기는 편은 아니지만, 실제로 사용자에게 서비스가 제공되었고 그들로부터 신기하고 재밌다는 피드백을 듣게 되니까 새삼 개발자로서의 초심을 되새기게 되었습니다.
‘아 맞다, 나 이런 피드백 듣는 게 뿌듯해서 개발자가 되었었지.’
아무래도 개발자로서 누릴 수 있는 최고의 뿌듯한 순간은 사용자가 겪고 있는 문제를 내가 직접 해결해 주고 그 감사함에 대한 피드백을 들을 때 라는 걸요. 글빼미도 그런 의미에서 제가 만들어본 프로젝트 중에서 초심을 찾게 해 준 의미 있는 프로젝트였습니다.
글빼미의 소스 코드는 이 곳에서 확인 가능합니다. 다만 개발할 때에도 외부 공개를 염두한 코드는 아니기 때문에, 코드의 퀄리티나 구조에 대해서는 저도 크게 신경쓰지 않고 만들어서 보기가 좀 그럴 수 있다는 점(?)을 미리 말씀드립니다. 😅