본문으로 건너뛰기

상호 존중하는 PR 만들기

· 약 5분
임채성(Puleugo)
2024 팀장, 백엔드

본 게시글은 주인공들의 이야기, 이한결님과의 인터뷰 내용을 참고하여 작성했습니다.
https://youtu.be/CQj797uQw1U?si=PmCScDRERUUNVmSI

Full Video

도입

최근 팀원에게 아래와 같은 코멘트를 받았습니다.

하나의 PR에 코드가 너무 많아요.
다음에는 조금 작은 단위로 PR을 만들어주세요.

이한결님과의 인터뷰에는 아래와 같은 답이 있었습니다.

  • 가독성 좋은 PR을 만드는 방법
  • 가독성 좋은 Commit을 만드는 방법

무엇이 상호 존중하는 PR인가?

무엇이 좋은 PR일까요? 예를 들면 "이거 바로 Approve해도 되겠는데?"라는 말이 나올만한 PR이 좋다고 할 수 있겠습니다.
좋은 PR을 글쓰기로 비유하면 PR은 문단, Commit은 문장이라고 생각해볼 수 있습니다. 가독성 좋은 글은 하나의 문단에는 하나의 주제, 하나의 문장에는 하나의 내용 만으로 구성됩니다. 글쓰기라고 생각하시면 아래 내용을 이해하기 편할 것입니다. 다시 말하면 문단(주제)이기에 Refactor PR과 Feature PR은 분리하는 것이 좋은 PR입니다.

커밋에 흐름 만들기

작업에 들어가기 전에 항상 체크포인트를 만들어야 합니다. 이 작업에 약 2000줄의 코드 변경이 필요하다고 가정했을 때 100줄 단위로 나누었을 때 어떤 흐름이 가장 안전하고 자연스러운지를 미리 설계하는 것이 중요합니다. 나뉜 커밋(작업)들 하나하나가 각 체크포인트라고 부를 수 있겠습니다.
팀원이 쌓인 커밋들을 봤을 때 곧바로 이해할 수 있도록 가독성 좋게 구성합니다.

하나의 커밋도 읽기 쉽게

한결님은 이를 약 400-500줄 정도로 유지하려고 하십니다. 융통성 있게 하면됩니다. 1000줄 이하라면 더 커지더라도 큰 문제는 없습니다.
하지만, 테스트코드는 필수적입니다. 한결님이 작성하신 500줄의 코드 변경은 아래와 같이 구성됩니다.

  • 100줄(20%): 기능 변경
  • 400줄(80%): (최대한 많은) 테스트 코드

이러면 외부 클래스/함수 의존성 때문에 테스트가 실패하지 않나요?

(영상에서는 간략하게 언급하고 지나갔지만) 기능없는 빈 메서드를 만들고, '이후 기능이 구현되었을 때 이러한 모습이겠지.' 를 생각하고 테스트를 작성합니다.

상호 존중하지 않는 PR은 Moloco 수석 개발자도 어려워한다.

1,000줄의 코드를 한결님에게 보낸다면 한결님은 다음과 같이 말씀하신다고 합니다.

나는 너의 코드를 보고 문제없다고 말할 자신이 없다..
너무 많은 Change를 한번에 넣었기 때문에.

Unit Test가 이미 많이 작성되어 있다면 이를 쪼개서 보내달라고 부탁합니다.

팀을 위한 좋은 습관

  • 장주영: "상대의 시간을 존중하는 좋은 습관같다."
  • 이한결: "그것도 맞지만, 이는 상호 존중이다. 내가 상대를 존중했을 때 이 존중이 나에게 돌아올 확률이 크다."

마치며

주인공들의 이야기는 학생 개발자로서 배워가기 좋은 채널이다. 누구나 잘하고 싶은 욕구가 있지만 경험없이 노력만으로는 잘하기 힘든 것들이 있다. 프로 개발자들에게 이러한 경험을 배워갈 수 있다는 것 자체가 축복받은 사회다.

이미지 로드 속도 향상하기

· 약 5분
임채성(Puleugo)
2024 팀장, 백엔드

개요

문제Waktaverse.games 사이트의 이미지 로딩 속도가 느려 사용자 경험에 부정적 영향을 미치고 있었습니다.특히 네트워크가 느린 환경에서는 LCP(Largest Contentful Paint) 시간이 권장사항인 2.5를 초과하여, Fast 4G 환경에서는 4.88초, Slow 4G 환경에서는 28.54초가 소요됐습니다.
해결방안이미지 로딩 성능을 개선하기 위해 Cloudflare를 활용하여 다음과 같은 조치를 취했습니다. WebP 형식으로 압축된 이미지 캐시를 응답했으며 페이지 새로고침 시 서버로 재요청하는 문제를 해결하기 위해 Cache-Control 헤더를 추가했습니다.개선 결과:- Fast 4G 환경: 5.88초 → 2.39초 (약 59.35% 개선)- Slow 4G 환경: 28.54초 → 8.24초 (약 71.13% 개선)
  • waktaverse.games 웹 사이트의 이미지 로드 성능 개선을 수행했다.

너무 느려요. 개선해주세요.

상혁이가 Waktaverse 이미지 로드 속도가 느리다고 문의메일을 보냈다.

동아리 친구에서 이미지 성능개선 작업 해보고싶다고 말하니까, 내가 속한 팀에 메일을 보내줬다.

어느정도로 느린가?

Fast 4G: 5.88 s, Slow 4G: 28.54 s

Chrome Browser의 Performance 기능을 활용하여 성능을 측정해보았다. 네트워크/메모리 성능을 제한하여 측정해볼 수 있으므로 성능 개선 필요 여부를 확인하는데 추천하는 방법이다.

LCP(가장 큰 콘텐츠 페인트) 소요 시간을 측정했다.

  • Fast 4G: 5.88s
  • Slow 4G: 28.54s

참고로 2.5초 이하가 GOOD이다.

해결하기

저희 팀은 Cloudflare CDN을 사용하고 있습니다. 사용하시는 CDN이 다르다면 아래 내용 중 무엇이 왜, 필요한 지만 참고해주시기 바랍니다.

1. 큰 이미지는 압축합시다.

흔히 사용하는 포맷은 png, jpg가 있지만, 웹 성능 향상을 위해 jpg보다 더 효율적인 압축 형식이 있습니다. 주로 WebP, AVIF가 있습니다.

Cloudflare에서 동일 이미지 URL에 대한 원본 이미지에 대한 압축본을 응답해주는 Cloudflare Polish 기능이 존재합니다.

[Cloudflare Polish | Cloudflare Images docs

Cloudflare Polish is a one-click image optimization product that automatically optimizes images in your site. Polish strips metadata from images and reduces image size through lossy or lossless compression to accelerate the speed of image downloads.

developers.cloudflare.com](https://developers.cloudflare.com/images/polish/)

2. 한번 받아온 이미지는 캐싱합시다. Cache-Control

현재 페이지를 새로고침할 경우 이미 로드한 이미지를 다시 불러오는 문제가 존재합니다. 이때 HTTP 응답 헤더 Cache-Control을 사용할 수 있습니다.

Cache-Control은 이미 수신한 리소스의 유효 시간이 지나기 전이라면, 브라우저가 서버로 새로운 요청을 보내지 않고 캐시로부터 리소스를 읽어와서 사용합니다.

리소스가 남아있기에 캐시로부터 리소스를 가져옴.

개선결과

Fast 4G: 2.39 s, Slow 4G: 8.24 s

  • Fast 4G: 5.88s → 2.39s (59.35%)
  • Slow 4G: 28.54s → 8.24s (71.13%)

'어떻게 해야겠다'는 명확했습니다.
Cloudflare가 이렇게 편한줄 알았더라면 훨씬 더 빠르게 작업에 들어갈걸 그랬습니다.

가장 후회하는 블로그 커스터마이징

· 약 3분
임채성(Puleugo)
2024 팀장, 백엔드

금년 4월 즈음에 다크모드를 대응하여 이미지 색상을 반전하는 기능을 구현하였었는데요.

[css를 활용한 다크모드 이미지 자동 대응

소개다음 영상을 보시면 무슨 말인지 쉽게 이해할 수 있습니다.아이디어https://github.com/joonas-yoon/boj-extended?tab=readme-ov-file GitHub - joonas-yoon/boj-extended: 백준 온라인 저지(BOJ)를 확장된 기능과 함께

ko.puleugo.dev](https://ko.puleugo.dev/190)

다크모드를 굉장히 좋아하는 사람 중 하나로써 제 블로그는 다크모드에 최적화된 환경으로 만들고 싶었습니다. 이는 현재 가장 후회하는 블로그 커스터마이징입니다.

재앙의 시작

뭔가 틀렸다는 것을 느낀 것은 동아리 사이트에 블로그 탭을 구현할 때 였습니다. 크롤링한 게시글의 이미지가 White Mode에서 볼 수 없는 문제가 발생했습니다. 색상 반전이 되지 않아, 이미지도 하얗고 배경도 하야니까요.
이는 곧 콘텐츠가 블로그의 css에 의존하게 되는 기이한 현상이 발생합니다.

이는 계왕권을 사용하여 다른 플랫폼에 글을 배포하게 되도 동일한 문제가 발생합니다.
아래는 Medium 플랫폼에 배포한 이미지입니다.

Medium에 동일한 글을 배포하면 이미지가 안보이는 현상 발생

"뭐.. 다크모드로 보면 되겠네."라고 생각하셨겠다면 Medium은 White Mode만 제공합니다. 우측은 Dark Mode Reader라는 구글 확장프로그램으로 CSS를 다크모드처럼 변경한 화면입니다.

정상적인 방법으로는 이미지를 볼 수 없습니다..

안하느니 못했나?

세상에 그런일이 어딨습니다. 이게 다 경험이니까요.

White Mode 기반으로 색반전 CSS를 변경하고 과도하게 꺠지는 이미지를 변경해야겠습니다.

[계왕권 출시] 당신의 블로그 가치를 44배 향상시켜주는 서비스

· 약 4분
임채성(Puleugo)
2024 팀장, 백엔드

당신의 게시글을 가치를 44배 향상시켜주는 서비스

프로젝트 소개

계왕권은 자동화 및 게시글 번역 배포 서비스입니다. 대표적 선진국 9개국의 인구수는 한국의 약 44배이므로, 단순 계산으로 당신의 블로그는 44배 이상의 영향력을 얻을 수 있습니다.

1500 조회수

왜 개발하게 되었는가?

저는 프로그래밍을 시작한 이후부터 국내 시니어 개발자들의 경험을 얻기 위해 강연, 스터디를 참여하고자 노력했습니다. 그분들의 공통된 조언이자 후회는 프로그래밍에 쏟은 노력을 외국어 학습에 쏟았다면 더 많은 기회를 얻을 수 있었을 것이라는 것이었습니다.
구글, 페이스북 같은 IT 기업의 헤드헌터에게 연락이 오더라도 영어능력의 부재로 인해 기회를 포기하는 경우도 있었으며, 본인들의 역량을 그들에게 전달하지 못하는 것이 가장 큰 아쉬움이었습니다.
외국어 공부를 대신해주는 것은 아니지만 비슷한 기회를 쉽게 창출할 수 있는 프로젝트입니다.

'계왕권'은 선배들에게 받은 조언 통해
노력을 배로 향상시켜주는 프로젝트입니다.

번역 결과물 미리보기

비교해보기
원글 | 번역글

수상할 정도로 높은 번역 퀄리티

퀄리티 높은 번역 게시글

공식 문서

[소개 | Kaio-ken Docs

Last updated 8 minutes ago

kaio-ken.gitbook.io](https://kaio-ken.gitbook.io/kaio-ken-docs)


QnA

무료로 사용할 수 있나요?

네, Github Action 통해서 사용하실 수 있습니다.

돈이 조금이라도 들 수 있나요?

네, ChatGPT API를 통해 번역하기 때문에 API 이용비가 발생할 수 있습니다.

이 서비스를 쓰면 조회수를 제외하여 구체적으로 어떤 이익이 있을 수 있을까요?

게시글 하단에 게시글 주제에 관련된 프로모션 링크를 삽입하는 방식으로 수익창출을 하려고합니다.
현재 기획으로는 아마존, 이베이, 알리 익스프레스, 클릭뱅크, 애플 어필리에이트, 쿠팡 파트너스가 있습니다. 금전적인 이익보다도 본인의 프로젝트나 PR이 해외에도 노출될 수 있는 것이 가장 큰 메리트입니다.

저도 기여할 수 있나요?

환영합니다. 현재 영어 블로그(Medium)밖에 지원이 안되므로 일어, 중국어, 인도어 등 여러 블로그의 전략패턴의 코드를 작성하는 것을 권장드립니다.

https://github.com/puleugo/kaio-ken

[GitHub - puleugo/kaio-ken: Automated Translation Development Post Distribution Application

Automated Translation Development Post Distribution Application - puleugo/kaio-ken

github.com](https://github.com/puleugo/kaio-ken)

[계왕권 프로젝트] 베타버전 개발기

· 약 6분
임채성(Puleugo)
2024 팀장, 백엔드

길고 험난했던 베타버전 출시

일일 1389 커밋

꽤나 막혔던 프로젝트였습니다. 새로운 프로젝트를 하는게 오랜만인지라 너무 추상적인 계획만 세우고 작업을 들어가서 구체화 과정에서 멀리 돌아간 작업들이 굉장히 많네요. 대표적인 것들만 정리해보겠습니다.

처음 생각했던 번역 게시글 업로드 후 본문을 수정하여 JS Injection 방식을 사용한 Link 방식은 문제가 많았습니다.

우선, 게시글 본문을 수정해야 하는 문제가 있습니다. 대부분의 블로그 플랫폼(Medium, Dev.to, Qiita, Tistory)의 API는 게시글 수정 기능을 지원하지 않으며 수정기능을 지원한다고 하더라도 JS Injection을 막아둔 경우가 대부분이었습니다.
Tistory의 API를 분석하여 Reverse Engineering을 통해 HTTP 통신만을 활용하여 게시글 수정을 구현하긴하였다만. 이는 너무 난이도가 높았습니다. 사실 Medium이 GraphQL 방식으로 통신하는 것을 보고 포기했습니다.

이방식은 포기하기 다른방식을 찾아봤습니다.

다행히도 Sitemap을 통해서도 Link가 가능하다.

구글에게 페이지의 번역본에 대해 알려주기」를 읽어보면 다른 방식들도 있습니다.

  1. HTML tag에 첨부. (실패)
  2. HTTP Header에 첨부. (플랫폼의 응답을 조작하는 방법이 떠오르지 않아 패스)
  3. Sitemap에 명시하기. ← 이 친구에 대해 알아봅시다.

Sitemap는 구글 크롤러에게 사이트의 페이지, 영상, 기타 파일에 대한 관계를 알려주기 위해 제공하는 파일입니다. 예를 들어 아래와 같은 정보를 제공할 수 있습니다:

  • 번역본 링크
  • 게시글의 마지막 수정일, 제목, 우선순위

굉장히 재미있는 파일입니다. Tistory에서도 제공합니다.
https://puleugo.tistory.com/sitemap

그럼 문제를 정의하면 '티스토리의 사이트 맵을 어떻게 수정하냐?' 일까요? 잠깐.. 아니죠. 문제는 '구글에게 제공할 내 블로그의 Sitemap을 작성하는 방법입니다.'
문제를 다시 정의하니 '개인 도메인을 발급하여 블로그와 연결하는 방법을 떠올렸습니다.' 개인도메인이라면 Sitemap을 마음대로 수정할 수 있을 것 같았거든요. 이는 정답이었습니다.

https://www.puleugo.dev/sitemap.xml

Vercel을 활용하여 Github Repository에 업로드된 sitemap.xml을 도메인에 연결하였습니다. 이러면 Free + Serverless + File System으로 작업 가능해졌네요! (5분정도면 작업가능합니다.)

2. 너무너무 많은 외부의존성

본 프로젝트는 프로젝트 규모 대비 외부의존성이 굉장히 많습니다. 간단히 나열해보자면:

  • Spread Sheet: 블로그에 대한 정보 입력
  • Github: 원 본⋅번역 게시글, 무결성 검증을 위한 Metadata 파일, Sitemap 파일
  • Github Action: 공짜 실행환경
  • ChatGPT: 영어 잘하는 형
  • Vercel: 무료 Sitemap 발사대
  • Blog Platforms:
    • Tistory: 구글 검색의 GOAT
    • Medium: 영어권 개발 플랫폼 GOAT

다시말해 테스트하기 굉장히 빡셉니다요. Github는 널널하기로 유명해서 그냥 Stub 안 만들고 작업했어요. Medium은 하루 사용량 초과로 429 던지는거 보니 Test Double이 시급하겠네요. 베타배포 후에는 테스트코드로 프로젝트를 조금 더 견고하게 만들어봐야겠습니다.

3. 늘 후회하지만 습관화는 안되는 것들 (고쳐야 할 약점)

  1. 코드짜기전에 시뮬레이션 더 많이 돌려보기
  2. 새로접하는 작업하는 것들은 방법 다양하게 찾아보고 가장 효율적인 것 생각하기.
    • Tistory RE한건 너무 무식한 방식이었다고 생각합니다요. (Reverse Engineering인지도 모르겠음.)
  3. 끝까지 테스트 작성하기.
    • 테스트 작성하기 힘들다면 책임분리가 잘못된 것이라는 것을 또다시 느꼈습니다.

마치며

내일 중에 베타버전 출시하겠습니다. 베타버전 배포가 끝나면 본격적인 취준을 시작해야겠네요..ㅋㅋ

Megabrain 동아리 블로그 탭 제작기

· 약 11분
서상혁(Singhic)
정회원, 백엔드

안녕하세요, Megabrain에서 백엔드 개발자로 활동하고 있는 Singhic입니다!

현재 보이는 블로그 탭이 어떻게 제작되었는지, 어떤 우여곡절끝에 만들어졌는지 알아보도록 하겠습니다.

글에있는 모든 코드는 Github Repository에서 볼 수 있습니다.

상상

현재 메가브레인 동아리 부원들의 각각의 블로그는 그저 회원 탭에서 한 명을 콕! 집어서 들어가야 블로그 하이퍼링크를 통해 볼 수 있게 되어있습니다.

여기서 한 번 더생각해 보면 굳이 이렇게 해야 하나? 그냥 블로그 탭에 자동으로 최신 글만 받아오게 만들 수 있지 않을까? 라는 상상에서 시작되었습니다.

구상

일단 기본적인 구상부터 시작했습니다. 블로그 탭을 만들려면

  1. 사이드바 - 블로그 탭 만들기
  2. 블로그 파싱해 오기
  3. 가져온 것을 Markdown 파일로 변환하기
  4. Github에 커밋올리기

간단히 이렇게 4개의 단계로 구상했습니다.

그럼 이를 바탕으로, 구체적으로게획을 세운 후 실행으로 증명해야 합니다.

구체적 계획 및 실행

  1. 사이드바 - 블로그 탭 만들기

현재 이 사이트는 Docusaurus 라는 페이스북에서 개발한 오픈 소스 문서화 도구를 사용 중입니다. 또한, React를 기반으로 만들어져있어 docusaurus.config.ts 라는 파일 안에서 쉽게 탭을 만들수있었습니다.

  1. 블로그 파싱해 오기

블로그에 있는 글들을 자동으로 가져와야 합니다. 이를 손수 맛있게 하면 좋겠지만 제가 갈려 나가는지라..... 그래서 fast-xml-parser패키지를 이용하여 파싱하는 Typescript 파일을 생성하였습니다. 또한 같은 문서가 있을 때 자동으로 예외 처리를할 수 있게 try, catch 문으로 작성하였습니다.

async function fetchAndParseXML(): Promise<void> {
try {
// XML 데이터 가져오기
const response = await axios.get(url);
const xmlData = response.data;

// XML 파서 설정
const options = {
ignoreAttributes: false,
attributeNameRaw: true,
};

// XML 파싱
const parser = new XMLParser(options);
const jsonData = parser.parse(xmlData);

if (hasDuplicate) {
console.log('No new files created: Duplicate content found.');
}
} catch (error) {
console.error('Error fetching or parsing XML:', error);
}
}
  1. 가져온 것을 Markdown 파일로 변환하기

2단계에서 가져온 것을 토대로 이제 파일을 만들어야 합니다. Docusaurus에서는 현재 Markdown 파일로 글을 생성, 작성 및 수정하게 되어있습니다. 그럼 이 가져온 글을 .md 파일로 만들어내야 하는데,, 이를 또 어떻게 해야 하나 찾아보는 중!

이게 또 패키지가 있네???

사실 많은 패키지중에서 html-to-md를 선택한 이유는

그냥 다른 방법들과 비교했을때 가장 변환된 결과값이 가장 정확하고, 에러없고, 누가봐도 가독성있게 짤수있었기 떄문!

async function saveDescriptionsAsMarkdown(jsonData: any): Promise<boolean> {
const htmlToMd = require('html-to-md'); // require로 가져오기
let fileIndex = 1;

// JSON에서 item 배열 추출 및 최신 항목부터 역순으로 처리
if (jsonData.rss && jsonData.rss.channel && jsonData.rss.channel.item) {
const items = Array.isArray(jsonData.rss.channel.item) ? jsonData.rss.channel.item : [jsonData.rss.channel.item];
const existingFilesContent: string[] = [];

// 기존 파일 내용 수집
for (let i = 1; ; i++) {
const filePath = `blog/authors_name/authors_name${i}.md`;
if (!fs.existsSync(filePath)) break; // 더 이상 파일이 없으면 중단
existingFilesContent.push(fs.readFileSync(filePath, 'utf-8'));
}

// 최신 항목부터 처리
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
const title = item.title || 'No Title'; // 제목 추출
const pubDate = item.pubDate; // pubDate 추출
const markdown = createMarkdown(item.description, title, pubDate);
const outputPath = `blog/authors_name/authors_name${fileIndex}.md`; // 파일 이름 생성
// 기존 파일 내용과 비교
if (!existingFilesContent.includes(markdown)) {
saveMarkdownFile(markdown, outputPath);
fileIndex++; // 파일 인덱스 증가
} else {
console.log(`Duplicate content found, not creating file: ${outputPath}`);
return true; // 중복 내용이 발견되었으므로 true 반환
}
}
}
return false; // 중복이 없었으므로 false 반환
}

Description을 기점으로 사이에 있는 모든 것을 받아오게 되어있습니다.

지금 생각해 보면 엄청난 돌머리였지만 그대로 코드를 올린 점이 하나 있는데..

주석에 최신 항목부터 처리. 이걸 왜 했을까.. 이것의 의도는 그냥 받아오니 옛날글이 위로 올라오네?? 그럼, 밑으로 내리려면 옛날 거부터 받으면 되잖아? 럭키비키구만 라고 했는데

조금만 더 알아보니 pubDate를 받아와서 적어주면 자동으로 해주더라고요.. 이것은 추후에 리펙터링하는걸로.

그리고, 혹시 기존에 있는 것과 새로 만들려는 파일의 내용이 같으면 굳이 안 만들어도 되니깐 if 문을 통해 확인 후 생성되게 했습니다.

  1. Github에 커밋올리기

이 단계는 따로 서버를 운영하고 있지 않은 동아리 사이트에서 어떻게 돌릴 것이냐, 라는 의문이 있었습니다. 이것을 위해 서버를 만들어? 그건 아니잖아

이것 또한 찾아보니 Github Action을 이용하면 가능하더라고요! Github Action이란 Github가 공식적으로 제공하는 빌드, 테스트 및 배포 파이프라인을 자동화할 수 있는 CI/CD 플랫폼입니다. 여기에서는 .github\workflows라는 폴더안에 .yml확장자로 된 파일을 만들어 워크플로를 자동화할 수 있었습니다.

name: Action name

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: "0 20 * * *"

jobs:
build:

runs-on: ubuntu-latest

steps:
- name: Set up chekcout
uses: actions/checkout@v2

- name: Set up node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'

- name: Install dependencies
run: |
npm install

- name: Run parser
run: |
node authors-parser.js

- name: Update or add .md file
if: steps.verify.outputs.changed == 'true'
run: |
git pull
git add .
git diff
git config --local user.email "email@email.com"
git config --local user.name "github_nickname"
git commit -m "Commit Message"
git push

일단 1번째로 branch를 설정했습니다. GitHub에서 호스팅하는 Runner 중에서 저는 ubuntu를 사용하였고, step을 기점으로 밑에서부터는 할 일을 시키는 공간입니다. 이름을 적고 어떤 걸 어떻게시킬 것인지를 적어주면 됩니다. 이름은 밑에 사진처럼 나오게 됩니다.

저의 계획은 순차적으로 노드를 가져와 패키지 실행을 위해 npm을 설치하고, 위에서 열심히 만들어둔 파일을 실행한 후 자동으로 커밋을 올리게 됩니다.

하지만 쉽게 된다면 말이 안 되죠. 악당 등장..

문제는 딱 하나. md 파일이 없어서 만들게 없어. 그래도 커밋을 올려야해? 라는 오류로 펑 * ∞ ...

이를 수정하기 위해 이리저리 돌아 아래와 같이 코드를 하나 추가했더니 성공해 버렸습니다

- name: Check if there are any changes
id: verify
run: |
git diff --quiet . || echo "changed=true" >> $GITHUB_OUTPUT

- name: Update or add .md file
if: steps.verify.outputs.changed == 'true'

딱 저 확인하는 절차 하나에 갈려 나갔습니다.

평가

사실 이 프로젝트 아닌 미니(?) 사이드(?) 정도 되는 장난감인데 저는 이렇게 오래 걸릴 줄몰랐거든요.. (사실 이게 5일이나 걸림)

처음으로 이렇게 경험해 보는 거고 군대 전역 후 복학하고 경험해 보고 싶어 이 기능을 구현하게 되었습니다.

주변에 조언도 많이 받고, 이 기능은 레포지터리를 삭제하지 않는 이상 계속 가지고 가는데, 이 글을 끝으로 마무리 짓는 것이 아닌 후에도 문제가 생기면 모두가 처리할 수 있게 리펙터링하고, 주석 자세히 달아두겠습니다.

괴물 메서드 리팩터링과 성능개선하기 | 안전하게 리팩터링하기

· 약 19분
임채성(Puleugo)
2024 팀장, 백엔드

개요

문제저희 팀은 Spreadsheet를 어드민 페이지로 활용하여 데이터를 관리하고 있습니다. 서비스 초기에는 빠른 콘텐츠 수집과 서비스 개발이 가능했지만, 서비스 규모가 커짐에 따라 관리하는 도메인과 필드 수가 증가하여 동기화 메서드가 복잡해졌습니다. 테스트 코드의 부재로 수정 시 부담이 너무 커졌습니다.
해결방안테스트 환경을 구축하고 기존 코드를 분석하여 테스트하기 쉬운 코드로 리팩터링했습니다.테스트라는 개념을 학습하기 위해 Test Double, Domain Model에 대한 게시글을 번역하고 정리하며 학습하였습니다. 테스트 후 문제없이 실서버에 배포되었습니다.

발표 영상


소개

안녕하세요. 왁타버스 게임즈의 백엔드 팀의 임채성입니다. 이번 글에서는 저희 팀의 오랜 과제였던 '구글 스프레드시트 동기화 메서드'의 리팩터링 과정과 성능 개선 방법을 공유하려고 합니다.

왁타버스 게임즈와 백엔드 팀의 역할

먼저 왁타버스 게임즈가 어떤 서비스인지 설명드리고 개발팀에서 달성해야하는 목표를 설명드리겠습니다. 저희 서비스는 유튜버 우왁굳의 메타버스 컨텐츠인 왁타버스의 팬 게임 ⋅ 어플리케이션 플랫폼입니다.

  • 팬 게임과 랭킹 그리고 도전과제 등 다양한 기능
  • 스프레드 시트로 어드민 페이지로 사용하기 때문에 DB와 데이터 일관성 제공
  • 24시간 무중단 운영
  • 팬 게임⋅어플리케이션의 랭킹과 다운로드 수 및 조회수 통계 제공

이러한 특성들을 고려했을 때 저희팀은 B2B와 디지털 플랫폼 비즈니스로 **서류작업과 파트너 팀과의 협업이 많은 팀**이라고 볼 수 있습니다. (엄밀히 따지면 비상업 프로젝트이기에 비즈니스는 아닙니다.)

스프레드 시트 데이터 동기화 메서드란?

플랫폼 비즈니스에서 가장 중요한 건 콘텐츠입니다. 개발 초기부터 스프레드 시트를 사용하여 팀 간의 의존없이 콘텐츠 수집과 서비스 개발이 이루어지며 왁타버스 게임즈 서비스가 빠르게 성장할 수 있었습니다. 따라서 왁타버스 게임즈의 기반이 되는 기능이라고 볼 수 있습니다.
서비스의 기반이 된다는 것은 무슨 의미를 가지고 있을까요? 어떤 기능보다 먼저 개발되었으며 가장 오래된 기능이라는 의미기도 합니다. 왁타버스 게임즈는 올해로 2년차에 접어들었고, 파트너 게임도 약 200개을 넘겼습니다. 트래픽도 초창기에 비해서 훨씬 많아졌습니다. 관리하는 도메인의 추가와 시트 내 필드 수 증가로 해당 메서드를 수정 가능하도록 리팩터링해야하는 상황에 도달했습니다. 그렇다면 데이터 동기화 메서드가 어떤 상태였는지 확인해보도록 하겠습니다.

괴물 메서드의 문제점

레거시 코드 활용 전략이라는 책에서 아래와 같은 문구가 나옵니다.

대규모 메서드는 다루기 힘든 수준이라면, 괴물 메서드는 재앙이라고 부를 만하다. 괴물 메서드는 너무 길고 복잡해서 손대고 싶지 않은 메서드를 의미한다.

왁타버스 게임즈 팀을 운영된 2년동안 이 메서드에는 수많은 도메인들이 추가되었고 복잡한 조건이 추가되면서 끔찍한 괴물 메서드가 되어 있었습니다. 데이터 동기화 메서드 코드의 상황은 다음과 같았습니다.

  • 코드 길이가 600줄을 넘음.
  • 테스트 코드가 없고, 수정 시 큰 부담을 줌.
  • 코드의 동작 범위를 완벽하게 이해하는 사람이 없음.
  • 복잡한 조건이 계속 추가되며 유지 보수가 어려워짐.

수정하는 입장에서 굉장히 부담스럽고 어려운 메서드입니다.
이제 스프레드 시트와 DB의 정보를 동기화하는 메서드를 괴물 메서드라고 부르도록 하겠습니다.

리팩터링하기

리팩터링하기 위해서는 기능의 요구사항을 쪼개서 이러한 순서의 사이클을 반복합니다.

  1. 분석하기
  2. 테스트코드 작성하기
  3. 리팩터링하기

분석하기

가장 중요한 부분입니다. 메서드가 어떻게 동작하는지, 어떤 부분에서 문제가 발생하는지, 이 기능이 이 메서드에 있는 것이 적절한 코드인지, 또한 기존 코드의 동작 기능인지 아니면 버그인지 분류해야합니다. 이 부분에서 중요한 것은 팀원의 코드를 분석하되 맹목적으로 믿지 않아야 합니다. 코드 내에서 버그를 발생 시킬 수 있을 것 같은 코드는 작업자분에게 여쭤보는 습관이 중요합니다.

간단히 살펴봤을 때 문제점은 이 모든 처리가 동기 ⋅ 블로킹으로 동작하고 있다는 것입니다.

개선할 부분이 보인다고 해서 이 코드를 바로 변경할 수 없습니다. 리팩터링하는 개발자는 코드의 그 함수의 역사와 영향 범위를 모르고 수정하는게 대부분입니다. 이럴 때 필요한게 안전한 리팩터링입니다.

테스트코드 작성하기

리팩터링이란 함수의 결과의 변경없이 코드의 구조만을 수정하는 방식을 말합니다. 하지만 리팩터링 또한 실수할 가능성이 없지는 않습니다. 이렇게 몇백줄이 넘는 코드를 리팩터링하는 경우에는 특히 더 실수가 많을 수 밖에 없습니다. 또한 문서가 없기에 코드 내에 어떤 기능이 동작해야하는 지도 정리되어 있지 않는 상황입니다.
이때 적용할 수 있는 것이 테스트코드입니다. 테스트코드는 리팩터링 시에 다음과 같은 실수를 방지해줍니다.

테스트코드란 정말 간단합니다. 기능이 의도대로 동작하는 지 검사해주는 역할을 합니다.

test('유저의 나이를 증가시킨다.', () => {
let user = new User({age: 1});
user.incrementAge();
expect(user.age).toBe(2); // ✅ user.age == 2
})

이를 통해 리팩터링 이 정상적으로 완료되었는 지 지속적으로 확인할 수 있습니다. 하지만 몇백줄의 코드 내에 테스트해야하는 기능이 얼마나 있을까요? 굉장히 많을 것입니다. 특히 애플리케이션 외의 DB, Redis, 3rd Party API 등과 커뮤니케이션이 있는 이 함수의 경우 테스트가 더욱 복잡할 수 밖에 없습니다.
그렇기에 이 함수를 테스트하기 쉬운 코드로 분리해야만 합니다. 이에 필요한 코드의 테스트 가치/난이도를 시각화한 표가 있습니다.

테스트 가치/난이도 시각화

현재 리팩터링하고자 하는 코드는 테스트 가치⋅난이도가 높은 '복잡한 코드'에 해당합니다. 이 코드를 리팩터링하기 위해서는 도메인 모델, 의존객체가 많고 간단한 코드로 분리할 필요가 있습니다. (자세히 알아보기: 만개의 테스트를 작성하지 마라. 202번째글)

그리고 처음 테스트코드를 작성하는 경우에 private 메서드를 테스트하려고 하는 경우가 있는데, 이는 옳지 않는 방식입니다. 제어 가능한 영역을 추가하거나 함수를 분리하는 방식을 고려해봅시다.

리팩터링하기

잘 분리되었다면, 서비스 레이어는 간단한 코드가 되고 비즈니스의 복잡한 부분은 도메인 모델에게 할당되게 됩니다.

// Application Service: 계좌 출금 예제
private TakeMoney(amount: number): void {
if(!this.atm.canTakeMoney) { // 인출이 가능한 지 확인한다.
throw AtmHasNotEnoughMoney('인출 불가');
}
const amountWithComission = this.atm.calculateAmountWithComission(amount); // 수수료 포함 금액을 계산한다.
this.paymentGateway.chargePayment(amountWithComission); // 금액을 청구한다.
this.atm.takeMoney(amount); // 인출한다.
this.repository.save(this.atm); // 저장한다.
}

간단히 요약하면 다음과 같이 역할이 분리됩니다:

  • 도메인 레이어: 모든 의사 결정자
  • 서비스 레이어: 도메인 레이어의 의사를 집행하는 집행자

이제 리팩터링을 수행해보겠습니다.

리팩터링 적용하기

1. 도메인 모델로 분리하기

저는 총 3가지 도메인 모델을 구현하였습니다.

  • Row(행): CSV 1ROW -> JSON, JSON -> DB QUERY, Validate, Numbering etc
  • Rows(행의 1급 콜렉션) -> FILTERING, UPSERT, etc
  • SpreadSheet(스프레드 시트) -> FULL CSV ROW -> Rows Array(3차원 배열)

다음과 같이 사용할 수 있습니다.

export interface SheetDto // DTO 정의
{
[SheetEnum.GAME]: Rows<GameRow>;
[SheetEnum.APP]: Rows<AppRow>;
// ...
}

// 시트 데이터 가져오기
function async getSheetData(sheetRange: Set<SheetEnum>): Promise<SheetDto>
{
const sheet = new SpreadSheet();

// 요청된 시트 범위가 없다면 초기값 반환
if (sheetRange.size === 0)
return sheet.values;

const rawRows = await this.googleService.getRawSheet(this.sheetId, sheetRange); // 원시값 요청
return sheet.fillRaws(rawRows).value; // 원시값을 가공하여 반환
}

// 메서드 사용
const {
GAME: gameRows, // 게임 행 데이터
APP: appRows, // 어플리케이션 행 데이터
} = this.getSheetData(new Set([SheetEnum.ALL]));
console.log(typeof gameRows); // Rows<GameRow>
console.log(typeof appRows); // Rows<AppRow>

gameRows.filterBy({edited: true}); // 수정된 데이터를 필터링
gameRows.upsert(gameEntities); // 데이터가 존재하면 업데이트, 존재하지 않으면 삽입
this.googleService.updateSheet(gameRows); // 구글 시트에 동기화

2. 쿼리 로직은 영속성 레이어로 분리하기

기존 레거시 코드에서 존재하던 문제점은 비즈니스 레이어에서 쿼리를 작성하는 행위입니다.

Layered Architecture 4Layer

다음의 예시와 같이 쿼리를 영속레이어로 이동시켰습니다.

class UserService {
constructor(
@InjectRepository(UserEntity)
private readonly ormRepository: Repository<UserEntity>, // 1️⃣ Framework에서 생성한 Repository의 인스턴스를 주입받은 변수
@Inject(UserRepository)
private readonly userRepository: UserRepository, // 2️⃣ 내가 등록한 UserRepository의 인스턴스를 주입받은 변수
) {}

addUserAge(userId: number)
{
this.ormRepository.createQueryBuilder() // ❌ 영속 레이어(UserRepository)에 작성하세요.
.update().set({ age: () => 'age + 1' })
.where({ id: userId })
.excute();
}

refactoredAddUserAge(userId: number)
{
this.userRepository.addUserAge(userId); // ✅ 복잡한 처리는 영속 레이어가 처리하자.
}
}

@Injectable()
class UserRepository {
constructor(
@InjectRepository(UserEntity)
private readonly ormUserRepository: Repository<UserEntity>,
) {}

addUserAge(userId: number)
{
this.ormRepository.createQueryBuilder()
.update().set({ age: 'age + 1' })
.where({ id: userId })
.excute();
}
}

리팩터링 결과

폴더 구조

wt-games:
├─sheet
│ ├─sheet.module.ts
│ ├─sheet.controller.ts
│ ├─sheet.service.ts # ⭐ 600 -> 200 lines
│ └─/domain # ⭐ NEW
│ ├─spread-sheet.ts
│ ├─rows.ts
│ ├─row.ts
│ ├─game-row.ts
│ ├─application-row.ts
│ └─etc-row.ts

├─game
│ ├─game.module.ts
│ ├─game.service.ts # 500 -> 300 lines
│ └─game.repository.ts # ⭐ NEW

├─application
│ ├─application.module.ts
│ ├─application.service.ts # 500 -> 300 lines
│ └─application.repository.ts # ⭐ NEW
...

메서드 구조

async private syncGame(gameRows: Rows<Game>): Promise<void>
{
// 1️⃣ DB에 동기화
const deletedCount = await this.gameRepo.deleteExcludeBy({ids: gameRows.ids});
// 2️⃣ SpreadSheet에 데이터 동기화
const editedGamesFromDb = await this.gameRepo.findEditedGames();
gameRows.syncWithDbChanges(editedGamesFromDb);

// 3️⃣ DB에 반영
const gameEntities = gameRows.toEntities;
await this.gameRepo.upsertMany(gameEntities);

// 4️⃣ Spreadsheet에 반영
const updatedGameRows = gameRows.updatedRows;
const updatedRowInfos = updatedGameRows.toRowInfos;
await this.googleService.updateGoogleDocument(this.sheetId, updatedRowInfos);
}

이와 같은 방식으로 리팩터링해줍니다. 복잡한 로직임에도 꽤나 가독성이 향상되었습니다.

성능 개선하기

과도한 API 호출(Excessive API Call) 개선하기

BULK 처리

유튜브 API 명세상 한번에 50개의 영상 데이터만 조회 가능.

간단하게 계산해보겠습니다. 실제 동작 성능과 정확한 지표는 아니고 가정된 상황에 대한 지표임을 알립니다.

가정

  • 총 조회할 영상의 수: N = 1,000
  • 한 번의 API 호출로 조회할 수 있는 영상의 수: 50
  • 각 API 호출에 소요되는 시간: 0.5초 (네트워크 왕복 시간과 서버 응답 시간 포함)
  • API 호출의 비용: $0.01/1000회 (예시로 설정한 API 요금 기준)

1. 기존 방식 (1회에 1개의 영상 조회)

  • API 호출 횟수: N = 1,000
  • 총 소요 시간: 1,000회 호출 * 0.5초 = 500초
  • 네트워크 비용: 1,000회 호출 * $0.01/1000 = $0.01

2. 개선된 방식 (1회에 50개의 영상 조회)

  • API 호출 횟수: ⌈ N/50 ⌉ = ⌈ 1,000/50 ⌉ = 20
  • 총 소요 시간: 20회 호출 * 0.5초 = 10초
  • 네트워크 비용: 20회 호출 * $0.01/1000 = $0.0002
항목기존 방식개선된 방식개선 비율
API 호출 횟수1,000회20회98% 감소
총 소요 시간500초10초98% 감소
네트워크 비용$0.0.1$0.000298% 감소

동기적 처리로 인한 병목 현상(Synchronous Bottleneck)

비동기적 처리(Asynchronous Processing)

이 방식을 적용해볼 때 중요한 것은 Node.js의 동작원리입니다. Node.js는 특징은 싱글스레드입니다. 데이터베이스와 논 블로킹, 비동기을 적극 사용하면서 훨씬 빠른 실행 결과를 얻을 수 있습니다.

중요한 것은 Promise.all 내부의 DB 커넥션을 고유하게 제공해야합니다. 만약, 하나의 DB 커넥션만 사용한다면 해당 커넥션을 사용중인 함수가 종료될 때까지 Promise Pool에서 대기하여 결과적으로 순차실행이 되기 때문입니다.

async syncSheet()
{
const
{
GAME: gameRows,
APP: appRows,
GAME_GENRE: gameGenreRows,
APP_GENRE: appGenreRows,
ACHIEVE: achieveRows,
BANNER: bannerRows,
GUIDE: guideRows,
BADGE: badgeRows,
} = await this.getSheetData();

await Promise.all
([
syncGames(gameRows, gameGenreRows),
syncApps(appRows, appGenreRows),
syncAchieves(achieveRows),
syncBanners(bannerRows),
syncGuides(guideRows),
syncBadges(badgeRows),
]);

const images = [gameRows.allImageIds, appRows.allImageIds, achieveRows.allImageIds, bannerRows.allImageIds, badgeRows.allImageIds].flat();
await this.s3Service.uploadFiles(images);
}

관련 실험 글 Promise.all 과 Transactions (feat. Node.js).

성능 개선 결과

구동 환경보다 성능이 좋은 환경에서 구동시간 결과입니다.

  • 실행 결과: 3s → 2.7s
  • 약 10%의 속도 개선

KAIST GDSC 해커톤 참여, 발표 회고

· 약 8분
임채성(Puleugo)
2024 팀장, 백엔드

참여 계기

재밌어보였다. 새로운 사람들을 사귀고 대화좀 하고 싶었다.

준비하면서

첫 해커톤 참여라 원활한 협업을 위해 보일러 플레이트 코드를 준비하려고 했다. 그러면서 '이상적인 개발자의 해커톤 준비 방법'이라는 글도 쓰고, 이전 주는 거의 해커톤 준비에만 투자했다.
사람들이 낯을 많이 가렸다. 소통을 위한 디스코드는 개최 일주 전부터 만들어졌는데 아무도 자기소개를 포함한 채팅 치지 않았다. 그래서 분위기를 띄우고 싶어서 자기소개를 길게 써봤다.

제발.. 하트좀..

해커톤 운영경험이 있다보니 열심히 준비하셨을 GDSC 운영진분들을 생각해서 굳이굳이 더 말눈광스럽게 작성한 것도 있다..

팀 매칭

팀 매칭은 GDSC 운영진에서 매칭해준다. 랜덤이지만 참가자 모두 좋은 분들이라 재미있었음.
우리 팀은 백엔드가 셋이었다. 나, 민주님, 윤정님 이렇게 백엔드가 세명이므로.. 팀 상황 상 Java로 진행됐다. (프론트는 민주님이 리드, 준비해간 Node.js Boilerplate 코드는 전혀 사용할 수 없게됐다.)
그리고 디자인, PPT, 기획자를 동시에 맡아주시는 우리들의 본체 성현님까지 4인팀 1팀이었다.

주제와 기획

주제는 UN의 17가지 지속 가능한 발전 목표를 충족할 수 있는 제품이었다. (개인적으로 주제가 여러개니까 기획이 까다로웠음) 다들 생각해온 솔루션이 있었지만 만장일치가 안 나왔습니다.
다들 기획자, 창업의 경험이 있었던지라. 까다로운 판단 기준이 있었습니다.

  • 참여자들의 공감을 받을 수 있는 문제를 해결할 것
  • 나부터 쓸 제품을 만들 것

그러다 10분을 남겨두고 정말 만족스러웠던 기획인 중고서적 교환 서비스를 개발하게 되었습니다.

기획 발표에 띄워뒀던 플로우

참고로 기획 발표는 2분이 주어졌으며 우리 팀은 2-3페이지의 발표자료로 발표했다.

해커톤에서

이번 해커톤에 참여하면서 가졌던 목표는 '좋은 사람들 많이 만나기'와 '재밌는 경험'이었다. 모두 이뤘어서 너무 좋은 경험이었다.
좋은 사람들 많이 만나기는 개발하며 내가 맡은 부분을 끝내면 (보통 2시간 간격정도) 다른 팀원분들과 대화를 10분정도 나눴다. 절반의 팀들과는 대화를 나눠본 것 같다. 너무 재밌었음. 팀원들과도 여러번 같이 걸으면서 다들 갖고 있는 경험, 고민들을 공유하면서 친해졌다.

왁타버스.. 감사합니다..

평소 외향적인 성격이 아닌데도 이번 해커톤에서 대화를 많이해봤던 것 같다. 그 이유를 생각해보니 애초에 왁타버스 관련 개발을한다고 말하니 다른분들이 쉽게 기억해주셨던 것 같기도 하다. 멘토분들이 서로에게 왁타버스를 설명하는것부터 너무 웃겼음.

버튜버 같은 걸 팀으로 운영하는거에요.
..버튜버가 뭐에요?
ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

발표

공업 고등학교 시절 전 몰랐어요.. 3년후에 카이스트에서 발표하고 있을지..

어떻게하면 사람들 기억에 남을지에 집중하면서 발표했던 것 같습니다. 좋은 의도의 기획이지만 프로덕트에 집중하면 당근같은 중고거래 플랫폼과 차별점을 잘 못 느끼실 것 같기도 했고.
시작부터 다른 발표와는 다르게 참여형으로 진행했다.

첫번째 퀴즈

  1. 밤세서 졸리시죠? 조금 재밌게 진행해보기 위해 퀴즈를 가져와봤습니다! (집중을 위한 간략 인사)
  2. 4월 4일이 뭔지 아시나요? (대부분 절대 모를 문제)
  3. ...
  4. 바로 종이 안쓰는 날입니다. 4월 14일이 블랙데이, 짜장면 먹는날인데 이건 아무도 모르시더라구용. 종이에 대한 관심이 적다를 말씀드리고 싶었어요..ㅋㅋ (일부러 언급)

두번째 퀴즈

  1. 그럼 이번에는 다들 쉽게 맞출 수 있는 문제(화제 전환)
  2. 여러분들이 생각하기에 "인류 역사상 가장 훌륭한 발명품은 무엇인가요?"
  3. 에어컨, 화약, 자동차, 책 등등..
  4. 다들 정답입니다. 그런데 지금까지 나온 발명품은 모두 인류의 행위를 도와주는 발명품입니다. 그런데 책만 유일하게 인류의 기억을 도와주는 발명품이다. 그런데 우리는 그런 책을 집에 쌓아두고 방치하고 있지 않느냐?

이후로는 프로덕트 설명..
참여자분들에게 발표평가는 좋았었지만, 심사위원, 멘토분들의 평가 반영률이 90%인지라 조금 발표방식을 바꿨으면 좋았을것 같기도 하다.

후기

재밌있었으니 대만족입니다.. (다음에는 Java로 된 Boileplate도 준비해가겠습니다..)

당신의 블로그, 계왕권을 쓸 수 있다.

· 약 7분
임채성(Puleugo)
2024 팀장, 백엔드

`

알파 버전 개발중

[GitHub - puleugo/kablog: Automated Translation Development Post Distribution Application

Automated Translation Development Post Distribution Application - puleugo/kablog

github.com](https://github.com/puleugo/kablog)

계왕권이 뭔데..

최근 취업 불황으로 인해 국내 개발자들의 평균 스펙이 향상되었습니다. 그러나 해외 블로그의 기술적 난이도는 국내보다 낮습니다. 예시로 일본의 대표적 기술 블로그 플랫폼인 Qiita 인기 게시글을 보면 이를 확인할 수 있습니다. (08/19 기준)

  • 【완전판】 이것 1개로 React의 기본을 마스터할 수 있다! 초보자 튜토리얼!
  • 【AWS 컨테이너 입문】간단한 Python 앱을 ECS에 배포해 보자!
  • Excel에서 CSV 파일을 '0 내림'이나 '문자화'없이 열기
  • 【Vue】VeeVallidate에 의한 커스텀 밸리데이션의 작성

그래서 '국내 개발자의 글을 해외로 수출하면 수요가 있지 않을까?' 라는 생각이 들었습니다. 대표적 선진국 9개국의 인구수는 한국의 약 44배이므로, 단순 계산으로 당신의 블로그는 44배 이상의 영향력을 얻을 수 있습니다. 드래곤볼의 계왕권을 떠올리며 프로젝트 이름을 이렇게 정했습니다.

딸깍 3배.

[출시 알림 신청을 받고 있습니다. 많은 관심 부탁드립니다!!]
[개발블로그 계왕권] 프로젝트 출시 알림 신청

설계 해보자

저는 초등학교 4학년때부터 11년간 블로그를 운영해 온 블로거입니다. 중학생 시절에는 블로그 마케팅도 경험했고, 네이버 블로그에서 누적 조회수 1,028k를 달성했습니다. (향로님의 약 10% 정도)

블로그 관련 콘텐츠에는 나름의 인사이트가 있다고 자부합니다. 이 관점에서 가장 큰 이슈는 다음과 같습니다.

이슈: 구글은 번역 글을 좋아하지 않는다.

구글은 유사 문서를 굉장히 싫어합니다. 당신이 며칠을 고민해서 작성한 글이라도 글의 내용이 유사하다면 검색결과 우선순위가 하락합니다. 특히 이미지를 동일한 이미지를 그대로 쓰면 우선순위가 하락합니다. 모든 검색엔진이 자체제작 콘텐츠를 좋아해요.

인기글 중 자체 콘텐츠(8/10)

그럼 어떻게 해야할까요?

해결 방안: 상호대체 여부를 명시하라.

사실 우리가 고민하는 내용들은 이미 선대 개발자들이 알잘딱으로 처리해놨을 가능성이 높습니다. 또한, 이번 케이스도 동일합니다. 각 포스트의 html head에 hreflang 태그를 삽입하면 국가에 맞는 게시글을 안내해주며 중복컨텐츠 문제가 해결됩니다. (명시 방식에는 다른 방법도 존재함)

요로코롬 넣어주면 굴루굴루에서 처리해줌.

게시글 작성할 시에 html 편집모드로 script 태그를 추가하면 됩니다.

<script>
document.addEventListener("DOMContentLoaded", function() {
var link = document.createElement('link');
link.rel = "alternate";
link.hreflang = "ja";
link.href = "https://example.com/ja";
document.head.appendChild(link);
});
</script>

내 취향의 기능 구현

  • 접근성은 엑셀이 최고: 모바일에서도 데이터를 조작할 수 있도록 구글 스프레드시트를 통해 데이터를 정리할 것입니다. 엑셀과 DB의 데이터를 CronJob을 사용하여 지속적으로 동기화할거에요.
  • 확장성은 markdown이 최고: html의 ui는 css 의존하므로 너무 의도치 않은 ui 결과가 나올 수 있습니다. 많은 글로벌 블로그에 배포하기 위해서는 github에서 markdown으로 관리 할 것입니다.
  • 서버리스로 작업: 편해지고자 하는 작업인데 서버는 관리하기 귀찮고 비용도 발생할거에요. 모든 처리는 Github Action으로 처리할게요.

아키텍처는 이렇습니다.

특히 마음에 드는 점은 추후에 다른 블로그 플랫폼으로의 마이그레이션에도 열려있습니다. 그래서 대표적으로 필요한 기능은 이렇습니다.

  1. 티스토리 게시글 md 변환
  2. ChatGPT 변역 기능
  3. 블로그 플랫폼 별 게시글 업로드 기능 구현
  4. 게시글 ↔ 엑셀 동기화*(조작할 필요은 적을 수록 가장 이상적)*

출시되면 많은 관심 바랍니다!!
(9월 중 오픈소스로 출시 예정)
[개발블로그 계왕권] 프로젝트 출시 알림 신청

[[개발블로그 계왕권] 프로젝트 출시 알림 신청

- 개인정보 수집 항목: 이메일 - 개인정보 수집 목적: 서비스 출시 시 알림 응답 제출 시 개인정보 수집에 동의하는 것으로 간주합니다.

docs.google.com](https://forms.gle/rQvbYyTxoeLdowmi6)

혹시 좋은 아이디어 있으면 위 구글 폼에 의견 달아주시면 감사하겠습니다.

개발자들아! 내게 아이디어를 조금만 나눠줘!!

감사합니다!!!

개발자의 이상적인 해커톤 준비 방법

· 약 11분
임채성(Puleugo)
2024 팀장, 백엔드

서론

KAIST GDSC에서 개최하는 스파클링톤에 참여하게 되었다. 모교에서 해커톤을 2회 주최한 경험은 있어도 100% 참여자로써의 해커톤 경험은 처음이라 많은 기대를 하고 있다. 해커톤 개최 경험자로써 개발자의 이상적인 준비를 이야기해보고자 한다.

협업에 관한 내용도 개발 실력보다도 아주아주 중요하지만 개발 이야기만 해도 할 말이 많기 때문에 이번 글에서는 생략하겠다.

해커톤 참여자는 이래야 한다.

해커톤은 스펙보다는 재미, 협업 능력 향상, 무료 피자와 콜라를 목표로 하는 것이 옳다. 번뜩이는 아이디어를 빨리 구현해야 하는데 대용량 트래픽, 확장성 같은 경험을 채우고 싶어서 해커톤에 참여하는 것이라면 그다지 좋은 팀원으로 평가되지는 않을 것이다.

그렇다면 어떤 개발자가 좋은 해커톤 팀원일까? 의존하지 않아도 되는 팀원이다. 다음은 이번 해커톤에서 모집된 직무들이다. (4인 1팀)

노드는 직무, 간선은 의존성이다. 채도는 의존도라고 보면 된다. 해커톤 내의 변수가 많으니 이 그래프가 절대적 지표는 아니다.
의존성이 있으니 백엔드 개발자 관점에서 어떤 것에 집중해야할 지 생각해 보자.

  • 기획자와 협업할 때, 해결해야 하는 문제와 솔루션을 확실하게 이해해야 한다. 확실한 이해를 위해 많은 의사소통을 해야 한다. 개발부의 의존성은 전적으로 당신에게 향해있다는 것을 명심하라.
  • 디자이너와 협업할 때, UI에 따라 내가 어떤 정보를 프론트엔드에게 제공해야 하는지, 사용자의 스토리에 따라 어떤 정보가 필요할지도 계속 생각해야 한다. 디자인에 따라 처리해야 하는 알고리즘을 서버에서 처리할지 프론트에서 처리할 지도 이후 회의해야 한다.
  • 프론트엔드 개발자와 협업할 때, 빠르게 API를 제공해야 한다. 사용자와 직접 의사소통하므로 가장 잔손이 많이 가는 작업을 맡으실 텐데 API 제공이 늦어진다면 그만큼 프론트엔드 개발자의 부담이 커진다.

다들 밤을 새우며 정신적으로 피곤해질 수밖에 없다. 해커톤을 개최하면서 싸우는 팀도 꽤나 봤어서 이를 방지하기 위해 좋은 팀원이 되는 것이 중요하다. 결국 참여자 모두 재밌고 행복하기 위해 존재하는 해커톤이니까.

백엔드라면 이것을 준비하라.

먼저, 보일러 플레이트를 준비하라. 보일러 플레이트는 거의 변경 없이 바로 사용 가능한 반복되는 코드를 말한다. 나한테만 도움 되는 것이 아니라 프론트엔드 개발자에게 도움 될 것이다. 예시를 들자면:

  1. API 문서(e.g. 스웨거)
  2. DB 세팅 및 ORM
  3. 배포되어 있는 AWS, S3 컨테이너: 프론트엔드가 서버를 클론해서 로컬에서 작업하는 것은 말도 안 된다.
  4. E2E 테스트: 단일, 결합 테스트는 미리 작성해도 거의 의미 없다. 어차피 기획이 나오면 변경될 내용이다. 해커톤 진행 시에도 E2E 테스트 외에는 개발이 완료되면 진행하자.
  5. Docker Compose를 활용한 빠른 3rd Party 확장
  6. 서버 자동 배포
  7. 게시글 작성
  8. 파일 업로드
  9. 채팅 기능
  10. 인증: 필요로 할 때 아니면 추가하지 말자. 보안 해커톤 같은 경우가 아니라면 프로젝트 개발 속도만 떨어뜨리는 족쇄다.

특히 E2E 테스트를 강조하는 이유는 당신의 신뢰성을 보여주기 때문이다. 해커톤에는 부담도되고 긴장도 되고 즐겁기도 평소와 다른 흥분한 상태일 것이다. 그런 상황에서 내 작은 실수가 크게 느껴질 수 있다.

이를 E2E 테스트가 검증해 줄 것이다. 스웨거 배포, API 동작 확인 등 프론트엔드 개발자에게 보여질 부분을 테스트하라. 또한 망각의 동물인 우리에게 테스트를 먼저 작성함으로써 제약사항이나 팀원끼리 정한 솔루션을 망각하는 실수를 줄일 수 있다.

두 번째로는 오픈소스와 외부 API를 사용해 보라. 해커톤에서 해결해야 하는 방법이 단순히 솔루션만 있다고 해결되지는 않는 경우도 많다. 이런 경우에는 "데이터"나 "복잡한 알고리즘"이 필요할 수 있다. 이런 부분은 오픈소스와 외부 API가 제공해 줄 것이다. 카카오의 구름톤의 교육에서 항상 빠지지 않고 나오는 주제이다.

사용방법을 읽어보는 것 또한 비용이다. 미리 할 수 있는 것은 해커톤 전에 한번 정도 사용해 보는 것이 옳다.

데이터 부분은 이를 참고하라.

외부 API는 이를 참고하라.

Node.js계열을 사용한다면 Nest.js 웹 프레임워크도 권장한다. 의존성을 Module 단위로 관리하기 때문에 보일러플레이트의 추가 기능 관리에 효율적이다.

세 번째로는 시제품(MVP)을 개발한다는 생각으로 프로젝트를 개발하여야 한다.

올바른 MVP

자동차라는 MVP를 만들 때는 자전거부터 시작하는 게 아니라, 최소한 자동차가 구현되어야 한다.

  1. 1번 케이스, 4단계까지 진행될 때까지 협업이 불가능하다.
  2. 2번 케이스, 우리가 만들고자 하는 기능을 확실하게 알라. 이동수단이라면 1단계로도 충분하다. 자동차라면 바로 4단계를 만들어라.
  3. 3번 케이스, 1단계부터 바로 목표였던 자동차를 만들었다.

3번 케이스가 MVP다. 많은 개발자들이 2번으로 착각하고 있는데 MVP는 최소 기능 제품이다. 굳이 최소 기능을 여러 번 분리해서 개발할 이유가 있을까? 바로 시제품을 만드는 것을 목적으로 하라. MVP는 변경될 여지가 적다.

더 중요한 것은 4단계에서는 동작하는 것이 확실하지만 1-3단계까지는 완벽하게 동작하지 않아도 된다는 점이다. 적절한 결과물(e.g. json)부터 빨리 프론트엔드에게 제공하여 개발이 진행되도록 하자. 프론트엔드 개발자도 서버의 API 처리 부분보다는 UI를 먼저 만들라.

네 번째로는 Git 버저닝 전략이다. 대부분의 개발자들이 Git Flow를 사용하는데 빠른 개발을 해야 하는 해커톤에서는 Github Flow 전략을 권장한다. Git Flow는 대규모 팀에서 적합한 반면 Github Flow는 1-3인 정도의 소규모 팀

Github Flow는 Main 브랜치 + n개의 기능 브랜치로 관리하는 방법이다. 운영 관련 브랜치가 포함된 Git Flow보다 훨씬 개발속도가 빠를 것이다.