기억의 실마리
2025. 11. 22. 03:08

✏️ 개요

사내 최초 in-house 서비스로 예정된 뷰티사업 관리 시스템 MVP 개발을 거듭 진행하다 문득 이왕 사내 in-house 서비스가 될 프로젝트인데 좀 더 신경 써서 최적화하고 싶다는 생각이 들었습니다.

 

재직 중인 회사는 시니어 개발자가 없고, 신입과 경력 3년 이하 주니어 개발자들로만 구성되어 있어 기본적으로 모든 최적화나 개발 노하우를 직접 찾아내어 공유하고 적용하지 않으면 기술의 부채가 심화되기 때문에 이를 경계하며 꾸준히 더 좋은 방향성을 가져가고자 부단히 노력해 왔던 것 같습니다.

 

주말 간 최적화 방법에 대해 탐색하다 보니 프론트엔드의 기술로 코드스플리팅, webp활용, preload, image lazy load 등 이미 적용한 최적화 기술들을 많이 소개 해주고 있었습니다. 그래서 이번에는 다른 영역인 DevOps 혹은 Infra 영역에서는 더 최적화 할 수 있지 않을까? 하는 생각이 들어 탐색하다 gzip, brotli, lz4 등 압축 포맷에 대해 알게 되었습니다.

 

그 중 가장 보편적으로 쓰이고 호환율도 가장 높은 gzip을 적용해보자 했습니다.

gzip은 아주 간단한 설정만으로 작동시킬 수 있었습니다. 빌드 시 번들러 설정으로 빌드 폴더에 .gz 확장자를 가지고 있는 브라우저에서 식별가능하고 압축해제를 직접 할 수 있는 압축파일을 포함시킬 수 있고, 웹서버에서 허용 설정을 추가해주면 정상적으로 빌드된 파일을 브라우저에 띄울 수 있다는 것을 알게 되었습니다.

 

프론트엔드 역량과는 무관하다고 볼 수 있지만, 저는 UX/DX 지향 개발자이기 때문에 이를 지나칠 수 없었습니다!

번들 사이즈 감소 빠른 다운로드 빠른 렌더링으로 UX 향상을 기대하며 실무에 적용하게 되었습니다.

 

🏠 Vite로 gz 파일 생성하기

gzip활용 설정 자체는 전혀 어렵지 않습니다.

vite.config.js 파일에서 default객체의 plugins 배열에서 compression 함수를 추가하고 아래 이미지와 같이 설정해 주면 빌드 시 .gz 확장자를 가진 압출 파일이 동봉됩니다.

 

vite compression 설정 추가

 

 

설정 후 빌드를 해보면 아래 이미지와 같이 청크파일과 .gz의 확장자를 가진 파일도 함께 생성된 것을 확인할 수 있습니다.

 

동봉된 .gz 파일

 

 

🌐 웹서버에서 .gz 파일 서빙

사내 인프라는 IaaS로 Ubuntu OS를 배포 환경을 ssh를 통하여 운영하고 있습니다.

통합 원격 접속 어플리케이션인 MobaXturm을 사용해서 리눅스 계정에 접속하여 NginX의 default 파일을 설정해 주었습니다.

 

보안상 NginX 설정 부만 캡쳐했습니다

 

매우 간단한 설정만을 추가하여 gzip 설정을 할 수 있었습니다.

 

🔍 index.js로 비교 측정 해보자

실제 사용 환경(Use Case)을 반영한 비교를 위해, 네트워크 속도는 조정하지 않은 상태에서 측정했습니다.

 

gzip 압축 전

 

번들사이즈: 1,025 KB

다운로드 속도: 794ms

 

압축 전에도 성능면에서는 나쁘다기 하기보다 오히려 좋은 수치에 속합니다.

 

gzip 압축 후

 

번들사이즈: 330 KB

다운로드 속도: 169ms

 

압축 후 실제 체감으로는 페이지 접속과 동시에 화면이 보이는 느낌이 들 정도로 매우 빠른 속도로 렌더링이 되었습니다.

그리고 마지막으로 LCP를 비교해 보았습니다.

 

gzip 압축 전 LCP

 

압축하지 않은 상태도 충분히 좋은 성능을 유지하고 있습니다.

 

gzip 압축 후 LCP

 

하지만 압축 후에는 더 압도적인 성능을 보여주는 것을 확인할 수 있었습니다.

 

✨ 정리하자면

  • 번들사이즈: [ 1,025 KB330 KB ] - 약 3.1배 감소 ( 67.8% )
  • 다운로드 속도: [ 794ms 169ms ] - 약 4.7배 단축 ( 78.7% )
  • LCP: [ 1.24s  0.58s ] - 약 2.1배 단축 ( 53.2% )

 

🤔 그렇다면 다른 파일은?

"혹시 index.js 파일만 이렇게 압축이 잘 된 것은 아닐까?" 궁금했습니다. 그렇다고 파일을 하나씩 열어보는 것은 너무 미련한 짓인 것 같아 최근 회사 복지로 지원받고 있는 Cursor AI를 활용하여 직접적으로 빌드 압축률을 계산할 수 있는 방법을 물어보았고, 덕분에 유용한 터미널 명령어를 받을 수 있었습니다!

 

우선 번들 파일들을 리스트로 확인 가능한 명령어를 실행시켜 보았습니다.

 

번들 파일 압축 리스트

 

확인해 봤을 때 직접 확인했던 index.js의 번들 압축률을 보니 거의 정확히 일치하는 것을 확인할 수 있었습니다. 추가적으로 번들 압축률을 살펴보니 전체적으로 압축이 정상 진행된 것 같아 우려했던 문제는 해결이 되었습니다.

 

아래 명령어는 번들 파일 압축 리스트를 모두 뽑아주는 명령어입니다.

# 반드시 빌드 명령어를 실행하여 dist가 root 경로에 존재해야 합니다.
# 그리고 경로는 루트 경로가 아니니, 꼭 확인해주세요!
cd C:\Users\{YourProject}\dist\assets

Get-ChildItem -Filter "*.js" | Where-Object { 
    Test-Path "$($_.FullName).gz" 
} | ForEach-Object {
    $original = $_.Length
    $compressed = (Get-Item "$($_.FullName).gz").Length
    $ratio = [math]::Round((1 - $compressed / $original) * 100, 1)
    
    [PSCustomObject]@{
        파일명 = $_.Name
        원본크기KB = [math]::Round($original / 1KB, 1)
        압축크기KB = [math]::Round($compressed / 1KB, 1)
        압축률 = "$ratio%"
    }
} | Sort-Object -Property 원본크기KB -Descending | Format-Table -AutoSize

 

추가적으로 통합 압축률을 계산해서 받을 수 있는 명령어를 실행하여 아래와 같은 결과를 얻을 수 있었습니다.

 

전체적으로 약 69.16% 압축된 것을 보니 압축 효율도 괜찮게 나온 것 같아 뿌듯했습니다.

그리고 아래 명령어는 전체 압축률을 받을 수 있는 명령어입니다.

 

# 프로젝트 루트에서 실행
cd C:\Users\{YourProject}

# 전체 압축률 계산 스크립트
$originalSize = 0
$compressedSize = 0

Get-ChildItem -Path "dist\assets" -Recurse -File | ForEach-Object {
    if ($_.Extension -eq ".gz") {
        $compressedSize += $_.Length
    } elseif ($_.Extension -match "\.(js|css|html)$") {
        # .gz가 있는 파일만 계산
        $gzPath = $_.FullName + ".gz"
        if (Test-Path $gzPath) {
            $originalSize += $_.Length
        }
    }
}

$compressionRatio = [math]::Round((1 - $compressedSize / $originalSize) * 100, 2)
$originalMB = [math]::Round($originalSize / 1MB, 2)
$compressedMB = [math]::Round($compressedSize / 1MB, 2)
$savedMB = [math]::Round(($originalSize - $compressedSize) / 1MB, 2)

Write-Host "==================================="
Write-Host "📦 빌드 파일 전체 압축 통계"
Write-Host "==================================="
Write-Host "원본 크기: $originalMB MB"
Write-Host "압축 후 크기: $compressedMB MB"
Write-Host "절약된 크기: $savedMB MB"
Write-Host "압축률: $compressionRatio%"
Write-Host "==================================="

 

🚩결과

가장 큰 성과는 "번들 사이즈 최적화로 이어지는 렌더링 속도 최적화" 입니다.

 

기대한 모든 성능이 2배 이상 향상되어 UX를 지향하는 저에게는 큰 성과였다고 생각이 듭니다.

간단한 설정 대비 얻는 효과가 큰 것 같아 사내 동료들과 공유하며 자연스럽게 더 좋은 최적화나 다양한 방법론에 대해서 발전된 이야기를 할 수 있게 된 계기가 된 것 같아 더욱 뿌듯했습니다.

 

앞으로도 동료들과 함께 성장하기 위해 더 노력하며 정진해야겠다는 다짐도 다시금 굳히게 되었습니다.

2025. 11. 20. 21:50

✏️ 개요

사내에서 가장 오랫동안 유지보수/고도화를 맡고 있는 🔗창업교육 플랫폼 Scout에서 고도화 기능 추가 요청사항 중에서 사업계획서를 플랫폼 스타일의 커스텀 프레임으로 출력하고 이를 PDF로 변환하는 신규 기능이 추가되었습니다.


초기 개발 완료 보고 이후에도 고객사 측에서 콘텐츠가 프레임 내에서 잘리는 현상이 반복적으로 발생하였고, 기능 담당자가 마감 내 구현을 완료하지 못하면서, 제가 해당 기능을 분석 및 재구현하는 역할로 투입되었습니다.

 

 

❓ 문제와 원인 탐색

해당 문제가 수면 위로 올라오기 전까지는 타개발 업무를 진행하고 있어 몰랐지만, 대표님께서 신뢰하는(?) 저를 불러 코드 한번 분석해 보고 피드백 달라고 전달해 주셔서 자세하게 버그가 나는 케이스를 찾기 위해 가장 먼저 QA를 진행했습니다.

 

다양한 케이스로 QA를 해본 결과 단순하게 작성한 글만 제대로 정상적으로 생성되고 줄 바꿈이 연속으로 일어나거나 이미지를 여러 개 추가하게 되는 경우에 콘텐츠가 들어갈 여백이 남아도 페이지가 넘어가거나, 잘리거나, 페이지 자체가 스킵되는 심각한 문제가 있었습니다.

 

코드를 직접 확인해 보니 바이브 코딩만으로 구현한 듯 보였고 관련 파일이 약 5개였는데 하나의 파일당 약 7000줄의 코드로 작성되어 있어 분석이 매우 어려운 상황이었습니다. 그래도 반나절의 시간을 투자하여 문제를 파악했습니다.

 

결과적으로 잘못된 기준으로 계산하고 있었고 이어지는 로직으로는 설계 자체가 기능을 구현할 수 없는 방향으로 구현되어 있다는 결론이 나왔습니다.

 

단순하게 풀 수 있는 로직을 반복의 반복을 반복하며 결과를 만들어내어 비효율적이고 혼잡하여 유지보수가 불가능한 수준으로 판단하게 되어 2주의 시간이 넉넉한 시간이 아닌, 터무니없이 부족한 시간으로 반전되는 순간이었습니다.

 

 

⛏️ 설계부터 다시

가장 먼저 본질적인 문제를 해결하기 위한 방향으로 설계하고자 했습니다.

다른 문제는 기본적으로 구현 난이도가 쉬웠으나, 커스텀 A4사이즈를 기반으로 json형태의 Text와 이미지를 잘림 없이 콘텐츠를 채우고, 페이지를 넘기는 기능을 구현하는 것이 생각보다 난이도 높았기 때문에 가장 우선 목표로 설계하였습니다.

 

 

🚨콘텐츠가 잘리거나 채우지 않고 넘어가는 문제

첫 번째로

가장 문제가 되는 부분은 바로 잘못된 대상을 기준으로 계산하는 문제였습니다.

단순히 초기 렌더링의 DOM을 기반으로 Layout 단계에서만 수치적 계산을 진행하고 실제 보이는 View와 미세한 규격 차이가 발생하여 제대로 된 페이지 넘김이나 생성이 어려운 문제가 있었습니다.

 

두 번째로

Text를 재귀하며 절삭하고 계산할 때 줄 바꿈에 대한 고려를 제대로 하지 못한 문제였습니다.

줄 바꿈을 절삭할 때 잘못된 로직으로 줄 바꿈이 절삭될 때 실제 요소에서 줄 바꿈의 높이만큼 취급되지 않거나, 단순히 공백으로 인식하는 문제가 있었습니다.

 

 

🔑 커스텀 PDF 프레임 규격 일치율 100%를 목표로

결과부터 말씀드리자면, 커스텀 PDF 프레임 규격 일치율 100%를 목표로 설계하고 개발하여 브라우저 View에서 보이는 수치를 정확히 반영하며 화면에 정상적으로 출력되는 PDF형 컴포넌트를 구현했습니다.


텍스트를 한 글자씩 계산/비교하며 오차와 리스크를 최소화하고, 콘텐츠 요소를 감싸는 wrapper의 height를 기준으로 내부 요소가 초과할 경우 남은 콘텐츠를 다음 페이지로 이어 재귀적으로 배치하도록 설계했습니다.


가상 measure 구조를 기반으로 카테고리별 콘텐츠를 점진적으로 커스텀 A4 프레임에 채우고, 모든 페이지 생성 완료 시 로직을 종료하며 각 페이지 우측 하단에 넘버링을 추가하여 차례대로 렌더링 되도록 구현했습니다.

 

✨ 정리하자면

  • 목표: 커스텀 PDF 프레임 규격 일치율 100%, 화면과 동일한 출력 보장
  • 접근 방법: 텍스트를 한 글자씩 계산/비교하며 오차와 리스크 최소화
  • 콘텐츠 Fit 처리:
    • 실제 콘텐츠를 감싸는 wrapper 요소의 height 기준
    • 초과 시 남은 콘텐츠를 다음 페이지로 이어 재귀적 배치
    • 가상 Measure 구조 활용: 정확한 위치와 크기 계산 기반
  • 페이지별 로직: 카테고리별 콘텐츠 점진적 배치 → 모든 페이지 생성 완료 시 로직 종료
  • 부가 기능: 각 페이지 우측 하단에 페이지 넘버링 추가
  • 결과: 안정적이고 규격에 맞는 PDF형 컴포넌트 렌더링 완성

 

 

🐢 완벽하지만, 느리다...

데드라인이 너무 가까워  "커스텀 PDF 프레임 규격 일치율 100%"를 최우선 목표로 정하고 안전성이 높은 방향으로 개발은 우선 완수했지만 안전성을 위해 구현했던 한 글자씩 넣으며 비교/계산하는 방식이 너무 많은 연산을 일으켜 Text 내용이 많아질수록 메인스레드에 부하가 커져 생성 완료까지의 시간이 너무 오래 걸린다는 문제가 있었습니다.

 

사실상 Text를 한 자씩 자르며 실제 요소에 넣어보고 비교하는 방식으로 생성하다 보니 어찌 보면 당연한 이슈였으며 극단적인 케이스로 얼마나 비효율적인 시스템인지 성능을 확인해 볼 필요가 있었습니다.

 

그 예시로 81페이지의 약 90% 이상이 Text로 가득 찬 사업계획서를 작성하여 테스트해 보았고 약 18초 정도소요되는 것을 확인하였고, 사용이 불가능한 수준이라고 판단되어 예정되어 있던 점진적 리팩터링을 시작했습니다.

 

 

💡 단순하면서도 본질적인 해법은?

가장 먼저 떠올린 것은 "이진 탐색 알고리즘"이었습니다.

한 글자씩 비교하는 본질적인 문제를 개선해야 하기 때문에 한 글자씩 탐색하는 것이 아닌, 최적의 해를 만들기 위해선 이진 탐색을 활용하면 좋겠다! 라고 생각했습니다.

 

다음으로 떠올린 방법은 raf( request animation frame )을 활용하는 것이었습니다.

 

 

🐇 신속한 아이디어 검증

이론적으로 확신을 가지고 떠올린 이진 탐색 알고리즘 반영은 콘텐츠 연산 관련 코드를 전체적으로 변경해야 하기 때문에 비교적 규모가 큰 반면,  raf를 적용하는 것은 비교적 간단하고 빠르게 변경/적용할 수 있어 우선 검증을 해보기로 했습니다.

 

결과적으로 생성까지 약 40초 이상으로 정도로 오히려 더 느려졌지만, 로딩스피너의 렌더링은 최적화되었으며 브라우저 자체 부하는 덜어진 것을 확인할 수 있었습니다.

 

두 번째로 떠올린 raf의 활용은 저의 오판이었습니다.

화면(렌더링) 최적화와 실질적인 연산의 최적화는 개념 자체가 다르다는 것을 순간 망각했었던 것 같습니다. 그렇게 검증까지 거친 후에서야 스스로 너무 안일했다는 생각이 들었습니다🥹...

 

raf는 렌더링 타이밍(fps)에 맞춰 매서드 등을 실행하기 위함이지, 단시간에 연산을 최적화하기 위해서가 아니라는 것을 다시금 마음에 새기는 계기가 되었습니다.

 

 

🚀 이진 탐색을 활용한 최적화, 그 결과는?

우선 수치화된 지표가 필요하여 측정을 위한 performance hook을 만들기로 했습니다.

 

간단히 구현한 performance 매서드를 활용한 측정 타이머 훅

 

 

위처럼 구현한 훅을 활용하여 먼저 이진 탐색 적용 전의 컴포넌트에서 PDF 규격 컴포넌트를 모두 생성하는데 얼마나 걸리는지 측정해 보니 아래와 같이 이전에 직접 세어본 18초와 유사한 18.7초 정도로 측정되었습니다.


알고리즘 적용 전 PDF 규격 컴포넌트 생성 완료 시간


 

 

다음으로 이진 탐색을 적용한 컴포넌트에서 생성 속도는 아래와 같이 1초도 채 되지 않는0.6초가 소요되었습니다.


이진 탐색 알고리즘을 활용한 PDF 규격 컴포넌트 생성 완료 시간


 

 

🚩결과

가장 큰 성과는 아무래도 "이진 탐색 알고리즘을 활용한 PDF 규격 컴포넌트 생성 완료 시간 단축" 입니다.

 

[ 18784.40ms 633.80ms ] - 30배 향상( 약 96.6% 시간 단축 )

 

위와 같이 알고리즘의 활용은 효율면에서 극단적인 차이를 보여 줄 수 있다. 라는 결과를 직접 적용하여 확인할 수 있는 경험이었고 어떤 상황에서 어떠한 알고리즘을 활용할 수 있는지가 매우 중요하다는 생각이 들었습니다.

 

직접 특정 알고리즘을 구현 할 수 있는 능력도 중요하다고 생각하지만, AI가 상용화되고 발전한 지금에서는 어떠한 상황에서 어떤 알고리즘을 활용하면 더 효율성을 높일 수 있겠구나. 하고 알고리즘 활용 아이디어를 떠 올리는 능력 또한 중요해진 것 같습니다.

 

그 만큼 단순 코딩 능력보다는 확장된 사고력을 유연한 창의력과 함께 문제를 해결하고, 더 좋은 성능을 구현하여 사용자에게 제공할 수 있는 능력을 가진 개발자가 미래에는 더 인정받는 좋은 개발자이지 않을까? 하는 생각이 들어 앞으로도 꾸준히 딥다이브 하고자 하는 마음이 굳혀지는 계기가 되었습니다.

2025. 11. 15. 17:57

✏️ 갑자기 서버, 그것도 Python 서버를 개선하게 된 계기

저는 프론트엔드 개발자입니다.

 

제가 재직중인 회사는 SI/SM 스타트 기업으로 스타트업 & SI/SM 회사의 특징으로 필요하다면, 혹은 해야한다면 응당 누군가는 반드시 기술을 구현해서 서비스를 제공해야합니다.

 

고객사 측에서 유지보수/고도화를 진행하고 있는 🔗LMS 플랫폼에서 작성한 사업계획서를 시장에서 가상 검증이 가능하도록 LLM을 탑재한 페르소나형 챗봇 서비스를 요구했고 고객사와 회의 중 대표님께서 기술 스택으로 확장성을 고려하면 Python이 좋겠다고 이야기 했고 이내 신입 Python개발자를 채용해주셔서 개발이 순조롭게 진행되는 듯 하였습니다.

 

하지만 급하게 다른 프로젝트가 생겼는데 마침 요구 스택이 Python이었고 그렇게 production에 배포하기 전에 신입 python개발자는 사무실을 떠나 파견을 가버렸습니다...

 

당장 급한 상황에서 Python을 개발해본 사람은 아무도 없었고 대표님께서는 혹시 해보고 싶은 사람 있냐고 물어봤지만... 말이 쉽지 사실을 또 다른 언어를 한다는게 부담스러운게 사실이었습니다.

 

저는  오히려 지금이 Node.js 외 언어 서버를 직접 경험할 수 있는 기회다 싶었고 어차피 퍼블리싱만 되어있던 챗봇 화면까지 같이 통합해서 작업하면 개발효율도 좋으니 챗봇 서비스를 통합 구현하겠다고 전달 드려 승인받았습니다.

 

다른 서버 포지션도 아니고 프론트엔드 개발자이지만 제 🔗취준생 시절 포트폴리오가 풀스택이었고, 🔗Next.js Serverless Architecture를 활용한 풀스택 개발 경험을 믿고 맡기신 것 같았습니다.

 

🔗그렇게 AI 시장 검증 챗봇 서비스를 통합 개발하게 되었습니다.

 

📘 기초는 이해하자

Python을 처음 접하는 만큼 서버가 돌아가고 있으니 기존에 개발되어있는 코드에서 추가 기능와 요구사항을 적용하며 배울 수 있겠다 싶었습니다. 그렇게 프로젝트 소스를 받아 열어보았는데 일단 기본적으로 언어 자체가 이해하기 어려운 구조는 아니었습니다.

 

다만, 개발되어있던 프로젝트의 아키텍처가... 없었습니다.

단일 폴더에 내부에는 app.py와 .env만 존재하는 형태로 상당히 당황스러웠던 기억이 있습니다. 아마도 실무에서 프로젝트 처음 진행해보고 같은 언어를 사용하는 선임이 없어 방향성을 잡지 못한 상태였고, 초기 개발단계라서 그랬던 것 같았습니다.

 

분명 Python에도 디자인패턴과 지향하는 아키텍처가 있을 것이고 Python이라는 언어에 대해서 어느정도 이해를 하고 있어야 분명 개발할 때 해메지 않을 것일거라 생각하고 하루 날을 잡고 구현하며 학습하였고 다양하고 방대한 학습 자료들이 있었기 때문에 실무에 바로 도입하기 위한 전체적인 프로세스와 필요한 기능을 구현하기 위한 지식을 학습하기 위해 선택하여 집중하기로 하여 아래와 같이 집중 학습 내용을 정리했습니다.

 

1. 프론트엔드 개발자로서 JS가 익숙하다. "기본 작동방식에 차이점이 있는가?"

2. 패키지관리 파일이나 폴더가 보이지 않는다? "보편적으로는 라이브러리 패키지 관리를 어떻게 하는가?"

3. 자바에는 유명한 MVC패턴으로 관심사 분리를 한다. "보편적인 디자인 패턴이 있는가?"

4. 개발기간이 넉넉한 것이 아니기 때문에 개발 속도도 중요하다. "나에게 익숙한 Nest.js와 유사한 패턴이 있는가?"

 

우선 기본적으로 싱글스레드 기반 블로킹방식으로 작동한다는 사실을 알게 되었고,  __init__.py 파일을 통해 내보내는 방식 또한 알게 되었습니다. 그리고 pip라는 패키지 매니저가 있으며, venv를 활용하여 node_modules처럼 프로젝트 단위에 패키지를 적용할 수 있다는 것을 알게 되었으며 " pip freeze > requirements.txt" 명령어를 통해 package.json처럼 프로젝트 패키지 스팩을 만들고 해당 스팩을 통해 명시 패키지만 다운로드할 수 있다는 사실을 알게되었습니다.

 

이렇게 하나씩 공부하며 챗봇 서버를 리팩터링을 우선으로 진행하였습니다.

 

📚 이가 없다면 잇몸으로, 커스텀 디자인패턴 도입

일단 개발기간을 산정했을 때 당장 MVP패턴과 유사하게 구성하는 것은 문제가 있었습니다.

때문에 4번째로 고민했던 부분인 나에게 익숙한 패턴이 있는가에 대해서 분석했을 때 디자인 패턴을 Nest.js와 유사하게 가져가기로 했습니다.

 

Controller, Service, Repository, Module로 관심사를 분리하려 했으나... 초기 러닝커브와 짧은 데드라인을 고려하여 Controller, Service만 관심사를 분리하기로 했습니다. 데이터 형을 지정하기 위한 class를 별도로 구성하지 않았던 상태여서 추가적으로 도입할 경우 꽤나 작업이 커지기 때문에 Model을 생략하였고(기존 코드 그대로 타입 직접 명시) Module의 경우 마찬가지로 Python에 대한 깊은 이해도 없이 접근하기에 무리가 있어 보여 생략하였습니다.

 

추가적으로 확장 가능성에 대한 부분도 염두에 두어 조금 더 세분화하고 관리에 용이하도록 src폴더로 감싸고 config, main, etc 등으로 커다란 카테고리를 구분했습니다.

 

커스텀 디자인 패턴 폴더 구조

 

임시 디자인 패턴이라 아쉽기도 하고 추 후에 Python 개발자 채용 계획이 있다고 하셨으니 그때 한번 제대로 배워보고 싶다는 마음가짐도 생겼습니다.

 

❓ LLM 챗봇인데 취소기능이 없다

현재 종사하고 있는 회사에는 기획자와 PM이 없습니다.

 

때문에 각자 주포지션은 있지만 기획은 매번 다 함께 할 수 밖에 없는 환경이다보니 놓치는 기획이나 당연히 있어야할게 없어서 뒤늦게 추가되는 경우도 종종 있었습니다.

 

이번이 그런 상황이었습니다. 디자인까지 모두 완성되었지만 취소 버튼이 디자인 되어있지 않아 개발을 하고 나서 QA를 하는데 "어라? 생각해보니 GPT에는 취소 기능이 있는데?" 라는 생각을 하게 되었습니다.

 

본질적으로 "왜 필요한가?"를 먼저 생각해 보았습니다.

 

스스로 떠올린 첫 번째 케이스는 바로,

당시 LLM API 특성상 응답이 꽤나 느렸고 만약 응답을 기다리다 응답을 받지 못한 상태에서 브라우저를 급하게 끄거나 다른 화면으로 이동하는 케이스를 생각해보았습니다.

 

그렇게 시간이 흘러 어느날 다시 챗봇에게 질문하기 위해 사이트에 접속해서 보았는데 "어라? 나는 이런 질문을 한 적이 없는데 왜 이런 질문과 결과가 있지?" 라는 불안감이 들기 시작할 수 있을 것 같습니다.

 

당시 상황이 급박하여 기억에서 희미해질 수 있기 때문에 오히려 요청 취소기능은 반드시 있어야겠다는 생각이 들었습니다.

 

두 번째 케이스로는 단순히 사용자가 잘못보내서 취소하고 싶은 경우입니다.

너무 당연한 이유이기 때문에 부연설명은 하지않겠습니다...

 

네, 이렇게 고민해보니 UX를 생각했을 때 당연하게 있어야 할 기능으로 보입니다.

 

때문에 이에 대한 문제를 공론화하고 개발을 진행하기로 했습니다.

 

❓ 요청 취소가 안된다

이유는 아주 단순했습니다. Python은 싱글스레드 동기 작동방식으로 Blocking 처리 방식입니다.즉, get 요청이 들어왔다고 가정하면 해당 요청이 끝날 때까지 다음 요청은 Blocking되어 실행되지 않고 기다렸다가 작업이 끝나야 다음 작업을 실행하게 됩니다.

 

그렇다면 취소를 하기 위해서는 작업중인 매서드에 간섭 가능하도록 제어권을 가져야 하기 때문에 이벤트 루프의 존재가 필수 불가결입니다.

 

기존에 사용하고 있는 라이브러리를 확인해보니 pysql, requests를 활용하여 모두 동기식으로만 처리하고 있어 요청 취소가 불가능한 구조였기 때문에 프론트엔드에서 제 아무리 AbortController를 통해 통신을 취소하려 해도 불가능했던 것이였습니다...

 

🛠️ Blocking ➔ Non-Blocking 전환

통신 관련 라이브러리를 [ pysql, requests  ➔ aiomysql, aiohttp ] 로 전환하여 비동기 처리가 가능한 awaitable 구조로 전환했습니다. 코드 전체를 확인하며 컨버팅을 해야 해서 꽤나 대규모 작업이었지만 할 수 밖에 없는 상황이니 만큼 컨버팅을 진행했습니다.

 

그렇게 http요청과 I/O 작업 시 await을 통해 작업 제어권을 가질 수 있는 구조가 되어 챗봇 요청 취소 기능을 구현했습니다.

 

추가적으로 non-blocking의 특징으로 동시성 처리 효율이 확보되어 많은 사용자가 동시간대에 요청을 했을 때 이전 구조보다 명확하게 응답대기 시간이 줄어들었습니다.

 

스스로 생각해봤습니다. 그렇다면 얼마나 차이가 날까?

그래서 K6를 활용하여 200건 동시 요청 통해 확인해 보았습니다.

 

Blocking 구조 - 12.5s

 

기존의 Blocking 방식은 200건 동시 요청 시 모두 응답받기까지 12.5초정도의 시간이 소요됩니다.

 

Non-Blocking 구조 - 7.4s

 

Non-Blocking 구조에서는 총 응답 시간이 7.4초로 감소약 1.7배의 성능 개선을 보여주는 것을 확인할 수 있었습니다.

 

❗ 로그인 상태를 검증하지 않는다

기존 구성되어있던 서버에서 회원 전용 기능임에도 요청 header에 담긴 토큰을 검증하는 로직이 없어 비로그인자도 요청하고 응답을 받을 수 있는 문제를 발견했습니다.

 

이는 매우 치명적인 문제로 즉시 도입하기 위해 기존 main API 서버인 Java 서버에 존재하는 토큰 검증 endpoint로 요청하여 공용으로 활용 가능하도록 auth_guard라는 데코레이터로 구현하고 재사용하여 문제를 해결했습니다.

 

🚩결과 정리

다음과 같은 문제와 해결책을 강구하여 성과를 얻었습니다.

  • 디자인패턴이 없이 app.py에 모든 코드가 작성되어 유지보수 어려운 형태
    ➔ 짧은 데드라인을 고려하여 가장 익숙한 Nest.js 디자인 패턴을 모방하여 커스텀 디자인 패턴 적용

  • 챗봇 요청 취소 기능이 없음
    ➔ 문제를 공론화 하여 기획 추가 및 기능 구현

  • Python 서버가 동기적으로만 작동하여 취소를 할 수 없는 문제
    ➔ [ requests aiohttp ], [ pysql  aiomysql ] 비동기 처리가 가능한 라이브러리로 마이그레이션
    ( 200건 기준 동시 요청 처리 효율 1.7배 향상 )

  • 로그인 상태를 검증하지 않는 문제
    ➔ 짧은 데드라인을 고려하여 main API Java서버에 존재하는 토큰 검증 endpoint로 요청하여 검증하는 auth_guard 데코레이터를 구현하여 재사용
2025. 11. 14. 23:20

* 본 포스팅 내용은 2025.01에 실적용한 기술이며, 도입 과정에서 작성해둔 내용을 기반으로 서술하였습니다.

✏️ 개요

사내에서 처음으로 프로젝트/테크 리더를 맡아 주도적으로 개발을 진행했던 🔗JA Korea - Finance Park 프로젝트에서 Three.js를 도입한 서비스를 개발하다 겪은 직접 QA를 하다 발견했던 Corner case Fix 경험담입니다.

 

우선 Three.js를 활용하게 된 계기부터 말씀드리자면 초기 기획에서 사용자가 학생이라는 점을 고려하여 재미있고 동적이었으면 좋겠다. 라는 의견이 나와 3D오브젝트의 활용까지 고려 가능한 Three.js를 도입하는 것이 좋을 것 같아 기술 제안을 하였고 "확장 가능성과 애니메이션 최적화를 고려했을 때 적합하다" 라는 의견으로 취합되어 채용하게 되었습니다.

 

사실 해당 프로젝트는 긴급하게 수주한 건이었고 약 2개월만에 MVP를 완성해야 하는 프로젝트였기 때문에 Three.js의 도입을 처음에는 망설였지만, 취업준비 당시 Thee.js를 독학하여 🔗3D 포트폴리오를 만들어본 경험이 있어 주도하여 대표님께 프로젝트를 맡겠다고 제안하였고 채택되었습니다. 긴급하게 고객사에 제공해야 하는 만큼 빠르게 개발하기 위해 Next.js + Prisma 기반 서버리스 아키텍처를 선택하여 긴급하게 시작했습니다.

 

❓ 나는 어떤 서비스를 개발하는가

우선 Finance Park에 대해서 소개드리자면 JA Worldwide 글로벌 재단이 존재하고 한국 지부로 JA Korea 사업단에서 주도하는 경제 학습 플랫폼입니다.

 

총 9가지 섹션의 소비 컨텐츠가 Map Object위에 존재하고 상호작용을하며 경제 활동 시뮬레이션을 통해 체험하고 예측 불가한 지출/수입에 대한 이벤트도 존재하며 현금과 신용카드를 활용하고 투자/예금 등 다양한 경제 활동을 시뮬레이션 할 수 있는 플랫폼입니다.

 

국제 사업단인 만큼 다양한 나라에서 저학년 학생들에게 학습 목적으로 Finanace Park 플랫폼을 만들어 서비스를 하고 있습니다.

 

저희가 레퍼런스로 잡은 플랫폼은 홍콩에서 제작한 🔗Finance Park - Hong Kong이었습니다.

 

기본적으로 시뮬레이션 입장 시 Map같은 화면을 움직이며 클릭하고 경제활동을 시뮬레이션할 수 있는 학습 서비스를 제공하고 있었습니다.

 

🚨 시작부터 난관

무난하게 첫 세팅을 끝내고 서버 작업을 본격적으로 시작하려 했지만 이전부터 주 SI/SM 고객사에서 추가적인 고도화 작업을 요구하여 서버작업은 일시 중단 되어 작업기간이 더욱 더 촉박해질 수 밖에 없는 상황이었습니다.

 

다행이 이를 인지한 대표님께서 인원을 추가 채용하여 제가 프론트엔드에 주력할 수 있도록 백엔드 포지션인 동료를 함께 참여 시켜주셨습니다. 덕분에 빠듯하고 빠른 이터레이션을 요구하는 상황에서도 좀 더 신경써서 프로젝트를 진행할 수 있게 되었습니다.

 

하지만 같이 프로젝트를 진행하게 된 새로운 동료분께서 현재 Next.js의 버전이 14인데 앞으로 유지보수를 생각하면 15버전으로 진행하는게 좋다고 하였고, 조금 걱정이 되었지만 수용하는 태도 또한 중요한 덕목이니 도전하는 마음으로 "이 또한 나의 성장의 밑거름이다!" 생각하며 즉시 15버전으로 마이그레이션을 하고 작업을 진행하였습니다.

 

허나, 이 것이 화근이었고 React + Three.js와 활용하기 좋은 React Three Fiber( 일명 R3F )가 아직 Next15를 지원하지 않는 것이었습니다. 그래도 괜찮을 것이라 굳게 믿으며 작업을 진행했는데 결국 모든 기능을 Next/React 스럽지 못하게 개발을 하게 되었습니다...

마치 Vanilla JS처럼 구현하게 되었습니다ㅠㅠ

 

다음 마이그레이션을 기약하며 아쉬운 마음으로 우선 작업기간이 짧은 만큼 기능 구현에 힘을 싣고 개발을 진행했습니다.

 

🛠️ Three.js Camera 포지션 제한, 제어 구현하기

우선 이왕 만드는거 레퍼런스 사이트보다는 더 향상된 품질로 만들고 싶었습니다.

저희 팀에서 확인했을 때 크게 3가지 문제가 두드러지게 나타났습니다.

 

    1. 시뮬레이션 화면에 입장 시 미친 듯이 돌기 시작하는 노트북의 팬
    ➔ 기본적으로 구현에 급급하였는지, 메모리 누수가 심각한 수준

 

    2. 사이드바에 가려지는 경제활동 컨텐츠
    ➔ 3가지 컨텐츠를 Map화면에서 즉시 볼 수 없고 사이드바를 접거나 직접 사이드바에서 접근해야 함

 

    3. 줌인과 줌아웃, 포커싱 등 Three.js를 활용한 Map화면 장점의 부재
    ➔ 동적이고 재미요소를 줄 수 있는 장점이 있음에도 활용하지 않고 단순하게 표현 부분이 아쉬웠음

 

우선 기본적으로 디자인 된 Map형태 이미지를 최대한 최적화하여 Webp로 추출하였고 Three.js를 활용하여 Plane Mesh를 만들어 이미지를 Map의 바닥 오브젝트로 구현하였습니다.

 

그리고 맵의 크기 만큼 카메라 포지션이 고정되도록 계산하여 제한하는 함수를 만들었고

문제는 이제부터 시작이었습니다.

 

💫 NaN... 이 수모를 잊지 않겠다...

뭐야, 왜 안돼?

 

분명 제대로 한 것 같았습니다.

만약 x, y, distanceZ params가 제대로 number형이 아니라면 typescript가 컴파일에서부터 에러를 발생시키기 때문에 제대로 문제가 생기면 에러를 발생시키기 때문에 디버깅은 문제 없을거라고 생각했습니다.

 

하지만... 이상하게 단위 테스트를 진행하며 개발하는데 어느 순간 제대로 작동하지 않고 카메라 핸들링 기능 자체가 멈춰버리는 상황이 생겨버렸습니다.

 

상세히 테스트를 해 보았고 기나긴 케이스 테스트 끝에 x, y, distanceZ가 마우스를 드래그한 채 브라우저 바깥영역으로 나갔다가 들어오는 경우에 브라우저 내 이벤트 입력 간격이 매우 짧아 졌을 때 x, y, distanceZ에서 NaN이 들어오게 되는 엣지케이스를 발견하게 되었습니다.

 

NaN는 number타입이기 때문에 typescript에서 에러를 발생시키지 않은 것이었습니다...

typescript로 number니까 undefined나 null이면 제대로 디버깅이 될 것이라는 생각을 한 것 자체가 안일했구나... 하며 반성을 하게 되는 계기였습니다.ㅠ

 

만약 String을 Number로 형변환을 강제하는 로직이라면 반드시 isNaN으로 검증하자! 라는 교훈을 얻었습니다.

 

isNaN을 추가하고 해결

 

이렇게 isNaN을 통해 x, y, distanceZ의 타입을 초입에 검증하는 방식으로 어느 순간 카메라가 핸들링 되지 않는 코너케이스를 해결한 경험을 쌓았습니다...

 

🧨 이번에는 맵 이탈...

이번에도 단위 테스트를 진행하다가 얻게된 코너케이스 경험담입니다.

줌 기능을 추가로 구현하고 여러 케이스를 테스트를 하다보니 어느 순간 맵 바깥 영역이 보이기 시작했습니다.

바로 최대로 줌인을하고 모서리 끝으로 이동 후 줌아웃을 하면 제한된 영역 바깥이 보이는 코너케이스가 발견되었습니다.

 

이 문제는 단순히 드래그앤 드랍으로 카메라 포지션을 x, y 축으로 제한 이동하던 프로세스를 기반으로 아이디어를 만들어 단위 정복하여 기능을 추가하고 해결했습니다.

 

  1. 휠 이벤트가 발생 시 줌 레벨을 기반으로 수치화된 값을 얻는다
  2. 수치화된 값을 제한된 Map 바깥 영역 수치에 넘어서지 않는지 검증한다.
  3. 검증을 기반으로 넘치는 수치와 방향을 저장한다.
  4. 저장된 방향과 수치를 Commit하여 실제 기능을 수행한다.

위와 같은 4가지 단계로 나누어 설계하고 프로세스를 구현하여 성공적으로 문제가 되던 코너케이스를 해결하였습니다.

 

🔗 코너케이스가 해결된 시연 영상

 

🚩결과

가장 큰 성과로 첫 번째는 "Number로 형변환 되는 경우 반드시 isNaN을 통한 검증 거치자"라는 교훈을 얻은 것이었습니다.

Typescript가 만능은 아니다. 라는 것을 깨닫고 zod를 활용하는 이유에 대해서도 더 알아보는 계기가 되었습니다.

 

두 번째로는 기능을 구현 할 때에는 단위정복을 해 나아가는 동시에 [ 계산단계 ] ➔ [ 커밋단계 ] 처럼 계산 후 마지막으로 적용하는 관심사 분리 설계에 대한 중요성을 다시금 깨닫는 계기였고, 이를 통해 React의 Fiber 아키텍처에서 Render phase와 Commit phase로 나누어 설계된 의미를 작게나마 이해 할 수 있었습니다.

2025. 11. 13. 23:59

* 본 포스팅 내용은 2024.09에 실적용한 기술이며, 도입 과정에서 작성해둔 내용을 기반으로 서술하였습니다.

 

✏️ 기술을 도입하게 된 계기

사내 첫 in-house service 예정 프로젝트를 개발하는 과정에서 생긴 문제로, MVP개발 단계이기 때문에 잦은 수정으로 프론트/백엔드 개발자 간 소통이 잦았으며, 매우 중요하기도 했습니다.

 

둘 중 한 포지션에서 실수를 하거나 변경사항을 전달하지 못한다면 오히려 소통의 비용이 더 늘어나고 작업기간 지속 지연되는 문제가 있었습니다.

 

문제점 요약

  • Swagger 문서와 실제 API 타입 간 불일치
  • 백엔드 스펙 변경 시 프론트엔드 타입을 직접 수정해야 하는 번거로움
  • 매번 axios 호출 함수를 수동 작성 → 반복 작업 증가
  • 실수로 인한 타입 오류 및 런타임 에러 발생

결국, API 변경 시마다 "이게 body에 들어가는 필드 맞나요?" 혹은 "string이 아니고 number인가요?" 등의 소통이 반복되었고, 개발 효율이 점점 떨어졌습니다.

 

 

DX / UX를 지향하는 저는 다시금 스스로에게 질문을 던졌습니다.

"이러한 문제를 어떻게 해결할 수 있을까?"

 

 

취업준비 기간에 만든 풀스택 프로젝트인 🔗ZETE - AI 메모 서비스를 개발할 때 그런 생각을 한적이 있습니다.

api를 만들고 Swagger에 spec을 명시하는데 이걸 다시 프론트엔드로 끌고와서 자동화 할 순 없을까?

 

병아리 개발자인 내가 이렇게 생각하는 기능이라면 분명히 아득히 먼 선배 개발자들이 모두 만들어 두었을거라는 가정을 가지고 탐색해 보았고 첫번째 검색에서 바로 찾을 수 있었습니다.

 

그리고 OpenAPI와 openapi-generator-cli 에 대해 알게 되었고 즉시 공식문서를 확인하며 긴 노력 끝에 React + Nest.js를 사용한 프로젝트에 적용시켜 이 후에는 월등히 빠른 속도로 풀스택 개발을 진행할 수 있었습니다.

 

그렇게 활용했던 openapi-generator-cli 를 도입하면 많은 것이 해결될 것 같다는 생각이 들어 동료과 논의 후 대규모 리팩터링인 만큼 기술건의의 주체인 만큼 책임감을 가지고 업무 외 시간(아무도 개발을 하지 않는 시간)을 활용하여 직접 구축하여 통합 적용했습니다.

 

🪜 도입과정

가장 먼저 openapi-generator-cli를 설치했습니다.

npm i @openapitools/openapi-generator-cli

 

🥄 꺼내기 ( Eject )

eject한 jar파일 경로

 

기존에 찾아내어 적용한 방식대로 설치 후 node_modules에서 templates 파일을 eject하기 위해 jar파일을 찾아내어 복사 후 압축을 풀고 mustache파일을 꺼냈습니다.

 

🪄 설정파일 & 명령어 추가

openapi-generator-cli 생성 명령어가 워낙 길기도 하고 auth / api 두가지 카테고리로 서버가 분류되어 있기에 작업하기 좋은 환경 즉, DX향상을 위해서 OpenAPI-CLI를 실행시킬 js파일을 아래와 같은 형식으로 구성했습니다.

require("dotenv").config({
  path: ".env.production",
});

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

const genObject = {
  auth: { url: process.env.SWAGGER_AUTH_URL, path: "./src/libs/openapi/auth" },
  api: { url: process.env.SWAGGER_API_URL, path: "./src/libs/openapi/api" },
  test: "test",
};

const current = genObject[process.env.GENERATE_TYPE];

// 제너레이트타입이 존재하지 않는 경우
if (!current) {
  console.error("GENERATE_TYPE 환경 변수가 설정되지 않았습니다.");
  process.exit(1);
}

// 명령어 실행 함수
execSync(
  `openapi-generator-cli generate -i ${current.url} -g typescript-fetch -o ${current.path} -c ./openapi.json --skip-validate-spec -t ./src/libs/openapi/templates/${process.env.GENERATE_TYPE}`,
  { stdio: "inherit" },
);

 

추가적으로 openapi.json, openapitools.json 두가지의 별도 설정파일이 더 있지만 생략했습니다.

 

명령어를 구성할 때 사내 배포환경이 [ dev, qa, prod ] 총 세가지 환경으로 분기되어 있어 환경마다 각기 다른 script 명령어가 필요했습니다.

 

다양하게 테스트 하고 실무에 바로 적용하기 위해

 

카테고리 개별 생성 ( import-auth / import-api ),

전체 생성과 삭제 ( create-api / remove-api ),

전체 삭제 후 전체 생성 ( reset-api )

 

3가지 스크립트 로직으로 구분짓고 엮어서 자동화 프로세스를 완성시켰습니다.

 

    "import-auth:dev": "dotenv -e .env.development -- cross-env GENERATE_TYPE=auth node openapi-generate.js",
    "import-auth:qa": "dotenv -e .env.qa -- cross-env GENERATE_TYPE=auth node openapi-generate.js",
    "import-auth:prod": "dotenv -e .env.production -- cross-env GENERATE_TYPE=auth node openapi-generate.js",
    "import-api:dev": "dotenv -e .env.development -- cross-env GENERATE_TYPE=api node openapi-generate.js",
    "import-api:qa": "dotenv -e .env.qa -- cross-env GENERATE_TYPE=api node openapi-generate.js",
    "import-api:prod": "dotenv -e .env.production -- cross-env GENERATE_TYPE=api node openapi-generate.js",
    "create-api:dev": "dotenv -e .env.development -- cross-env pnpm run import-auth:dev && pnpm run import-api:dev",
    "create-api:qa": "dotenv -e .env.qa -- cross-env pnpm run import-auth:qa && pnpm run import-api:qa",
    "create-api:prod": "dotenv -e .env.production -- cross-env pnpm run import-auth:prod && pnpm run import-api:prod",
    "remove-api": "rimraf ./src/libs/openapi/api ./src/libs/openapi/auth",
    "reset-api:dev": "pnpm run remove-api && pnpm run create-api:dev",
    "reset-api:qa": "pnpm run remove-api && pnpm run create-api:qa",
    "reset-api:prod": "pnpm run remove-api && pnpm run create-api:prod",

 

꽤나 장황해 보입니다... dotenv와 각 환경 변수를 활용하여 작성하였습니다.

당시 저의 최선이었지만 분명 더 좋은 방법이 있을 것이라고 생각합니다🥲

 

🥸 API를 생성하는 템플릿 Mustache 파일 커스텀

첫인상이 폭력적인 mustache파일...

 

이게 뭐지 감도 안잡힌다 느낌이었지만 자세히 보면서 하나하나 주석을 남기며 어떤 부분이 어떤 역할을 하는지, 어떤 구문 어떤 코드를 생성해내는지 등 분석하여 기존 사내 프로세스와 커스텀 펫처 등이 정상작동하도록 templates를 커스텀 했습니다.

당시에는 ai를 제대로 활용하지 못했고 cursor - ai도 사용하지 않았기 때문에 주석을 남기며 [ 생성 / 검토 / 수정 ]을 반복하며 작업했기 때문에 좀 더 고된 경험이었습니다...🥹

 

고된 경험1: 주석을 남긴다.
고된 경험2: 생성하고 확인한다.

 

이렇게 openapi-generator-cli 를 활용한 api 생성 자동화를 적용하였습니다.

 

🚩결과

결과적으로는 3가지 이점을 얻을 수 있었습니다.


첫 번째로 불필요한 커뮤니케이션 제거되었습니다.

login api - body에 들어가는 필드 중 validateFlag가 number -> string으로 바뀌었어요!

➔ login api 변경사항 있어 재배포 합니다!

 

구체적인 설명이 필요 없이 특정 엔드포인트의 재배포 라는 단어만으로 프론트엔드는 npm run reset-api:dev 등의 명령어로 간단하게 수정사항이 반영된 api를 받아 빠르게 직접 확인할 수 있게 되어 커뮤니케이션 리소스가 감소했습니다.


두 번째로 유지보수 편의성이 향상되었습니다.

 

예시 변경 사항:

login api에서 env: "dev" | "qa" | "prod" 와 같이 req body 필드에 추가하고

response를 env에 따라 level: "noob" | "normal" | "super" 과 같이 내려주어야 하는 추가 기능이 생겼다.

 

[ Before ]

1. 변경되어야 하는 req의 필드 키 네임과 값의 타입 확인

2. req body에 정의된 interface 수정

3. api의 req body 필드를 변경사항과 동일한 필드로 수정

4. reponse에 정의된 interface 수정

 

[ After ]

1. npm run reset-api:dev

2. api의 req body 필드를 변경사항과 동일한 필드로 수정

 

하나하나 수정해 줄 필요 없이 명령어로 리세팅 후 req body만 변경해주면 되기 때문에 편의성이 크게 향상되었습니다.


세 번째로 코드 품질과 일관성이 향상되었습니다.

협업 과정에서 개발자마다 서로 다른 코드 스타일이 혼합되면 일관성이 떨어질 가능성이 높습니다.

그러나 openapi-generator-cli 기반 자동 생성 방식을 도입하면서, 검증된 템플릿을 기반으로 일관성 있는 api 생성이 가능해졌습니다.

2025. 11. 2. 18:17

나는 프론트엔드 개발자다.

이 사실은 분명하고 재직중인 회사에서도 프론트엔드 개발자로서 일하고 있다.

그리고 내가 개발한 것을 돌아 보았을 때는 꽤나 많은 기능과 화면을 개발한 것 같았다.

 

주로 LMS를 개발하였고

[ 학습 프로그램 개설, 설문조사 기능,

발급기관명이 찍히는 수료증 발급,학습 프로그램 관리, 사업계획서 작성,

사업계획서 pdf렌더링, 사업계획서 시장 검증 챗봇 ]

등 다양한 frontend를 구현했다.

 

frontend 개발자로서의 길을 잘 닦고 있다고 생각하고 있었지만,

다시 한번 잘 생각해보니 개발자라면 시키면 누구나 할 수 있는 작업이

대다수인 것 같다는 생각이 들기 시작했다.

 

단순히 어떠한 기능을 구현했다는 단순함 보다는 개발자의 역량으로서

어떤 기능을 어떻게 이해하고 구현했는지 등이 중요하다는 생각이 들었고

좀 더 딥다이브가 필요하고 더 확고한 방향성을 구체적으로 잡아야겠다는

생각이 자리잡았다.

 

가장 우선적으로 "Why"에 좀 더 집중해보기로 했다.

지금 이 글을 작성중인 블로그는 사실 기술 블로그로서 활용할 계획이었다.

 

그 이유는 AI가 사용화 되기 전에는 직접 구글링을하고 stack overflow에서

검색하며 트러블 슈팅이나 기술에 대한 이해도를 좀 더 명확히 이해하고 기록하고자

기술 블로그를 목적으로 개설하여 사용하고 있었다.

 

하지만 현 상황을 생각하며 "왜 필요한가?"를 떠올려보면 "글쌔..." 라는 생각이 들기 시작했다.

 

하지만 어째서 글쌔라는 답변을 스스로 하게 되었을까?

 

바로 AI의 활용 때문이다.

 

정보에 대한 것을 포스팅하는 것은 더 이상 무의마하다는 생각이 들기 시작했다.

때문에 이 포스팅을 기점으로 개발 과정에서 있었던 이야기를 서술하는 방식으로

방향성을 잡아보고자 한다.

 

어쩌다 보니 Infra? DevOps?

최근 나의 개발 이력을 정리하다보니 인프라, 데브옵스에 관련된 작업을 많이 하게 된 것 같다.

과연 장래 프론트엔드에 도움이 될 내용인가? 라는 자문을 했을 때

"글쌔..." 라는 답변이 또 나오게 되었다.

 

그렇다고 이러한 개발 과정이나 경험이 무의미하거나 필요 없나?

"아니" 라고 확고하게 이야기 할 수 있었다.

 

결국 체계가 잡혀있지 않은 스타트업에서는 누군가 체계를 어느 정도

잡을 필요가 있었고 그 역할을 내가 주도적으로 맡았기 때문에 자연스럽게 인프라와

데브옵스에 대해서 개발/작업을 해왔던 것 같다.

 

이제는 체계가 어느 정도 잡혀서 좀 더 프론트엔드 개발과 프론트엔드 딥다이브에

힘을 실을 수 있을 것 같단 생각이 들었고 앞으로 좀 더 체계적으로

방향성을 잡고 계획을 통해 딥다이브를 해볼 생각이다.

 

그리고 얼마전 SRE 라는 직무에 대해서 알게 되었다.

이 직무는 Server / Infra / DevOps 이 세가지 업무를 능통하게 다루며 개선시키고

확장시키는 직무라는 이야기를 듣게 되었다.

 

하지만 나는 프론트엔드 개발자이다. 그렇다면 프론트엔드에는 비슷한

복합직무가 없을까? 하고 찾아보던 도중 채용공고에서 종종 보았던

FrontOps, Platform Engineer, Fullstack Engineer 등이 비슷한 역할을

하고 있다는 사실을 알게되었다.

 

이제는 좀 더 포괄적인 개발 경험 덕분에 뛰어들 수 있는 개발 포지션의 폭이

넓어졌다는 긍정적인 생각을 할 수 있게 된 것 같다.

 

아무튼 딥다이브!

결론은 딥다이브를 해야된다. 무엇이든 유추와 표면적 흐름을 이해한다기 보다는

어떤 이유에서 이러한 작동을 하게 되는지, 어떤 개념에 의해 이러한 프로세스를 지향하고

어떤 방향성을 지양하는지 등 딥다이브의 중요성과 필요성을 많이 느끼게 되는 요즘이다.

 

스스로 생각했을 때는 결코 게으름을 피우거나 잘못된 방향으로 개발해왔다고 생각하지 않는다.

하지만 어디까지나 스스로 생각했을 뿐이다.

 

나는 좀 더 좋은 개발자가 되고싶다.

그렇기 때문에 스스로가 아닌, 마치 프로그래밍처럼 누가 생각해도 확고하게

방향성을 가지고 정론과 확장가능성을 이야기하고 개발을 통해 실현할 수 있는 더 나은

가치에 대해 이야기하는 개발자가 되기 위해 반드시 필요한 과정이라는 생각이 떠나지 않는다.

 

나만의 만족이 아닌 함께 하는 이들과 함께 더 나아갈 수 있는 개발자로 거듭나기 위해

딥다이브는 별도의 개념이 아닌, 기본자세가 되어 방향성을 바로 잡겠다고 다짐해본다.

2025. 9. 30. 02:10

🤔지원하게 된 계기

최근 반복되는 일상속에서 더 성장하기 위해서는 새로운 자극이 필요하다는 생각이 들었고, 마침 멋쟁이 백엔드 개발자 친구의 소개로 구름톤이 이번에 참가 인원을 모집하고 있다는 것을 알게 되어 신청하게 되었다.

 

"서로 다른 환경에서 성장하던 동료들과 소통하며, 극단적으로 짧은 기간에 MVP를 완성시킨다." 라는 점에서 분명히 새로운 경험을 기반으로 서로의 애티튜드를 배우며 성장할 수 있는 기회를 얻을 수 있을 것이라고 생각했고 생에 처음으로 구름톤에 신청하게 되었다.

참가 신청은 Google Form으로 몇가지 문항에 대한 답변을 작성하여 제출하는 방식이었고,

주요 문항은 아래와 같다.

 

  1. 사용 가능한 프레임워크/라이브러리(React, Vue 등)를 나열하고, 그중 가장 자신 있는 기술을 하나 골라 이유를 설명해주세요.(공란 포함 최대 300자)
  2. 최근 6개월 내에 가장 많은 시간을 등려 해결했던 기술적 문제를 작성해주세요.(공란 포함 최대 300자)
  3. 현재 관심 있는 프론트엔드 기술이나 개념을 작성해주세요.(공란 포함 최대 300자)
  4. 선호하거나 관심 있는 디자인 시스템을 작성해주세요.(공란 포함 최대 300자)
  5. 구름톤 참여 동기를 작성해주세요.(공란 포함 최대 300자)
  6. 구름톤을 통해 어떤 부분의 '성장'을 기대하고 있는지, 있다면 작성해주세요.
  7. 협업 시 지키는 본인만의 규칙이나 전략은 무엇인가요? 갈등 상황이 발생했을 때의 해결 방법도 함께 알려주세요.
    (공란 포함 최대 500자)
  8. 진행하신 프로젝트 중 하나를 선택하여 설명해 주시고, 그 경험으로 인해 얻을 수 있었던 결과를 작성해주세요.
    (공란 포함 최대 500자)

사실 문항 내용을 보고 생각보다 디테일한 설명을 요구하고 글자 제한도 있어 답변하기 꽤나 어려웠다... 약 1시간 30분정도 작성했던 것 같다.

 

그렇게 제출하고 1주일이 지난 후 합격 통보를 받게 되어 급하게 대표님과 면담하여 5일 연차를 승인 받아 구름톤에 참가 확정서를 제출하였다.

 

당시에는 몰랐지만 지원자 수가 약 400명 이라는 말을 들었고 문항답변을 좋게 봐주신 것 같아 너무 감사했다...🥹

 

내가 글을 썼을 때의 포인트는 핵심 내용 위주의 서술과 가능한 수치화하여 표현하고, 새로운 경험을 통해 배우고 싶다는 진심을 많이 어필했다. 그리고 4번 질문에 디자인시스템에 대한 내용이 있었는데, 현재 회사에는 디자인 시스템을 활용하지 않고 있어 직접 구성중이며 이번에 참가하게 되면 많이 배우고 싶다고 서술한 내용이 있다. 아는척 하는 것 보다는 솔직한 마음으로 "배우고 싶다" 를 어필하는게 더 좋을 것 같다고 생각하여 글에 녹여냈는데 진심이 잘 통한 것 같아서 뿌듯했다.

 

📅구름톤 15기 일정표

이전 14기 일정도 찾아보았는데 크게 달라진건 없어서 참고할 때 큰 도움이 되었다.

 

🤼구름톤 여정

Day - 1

처음에 간단히 구름톤에 대한 소개와 멘토진 소개 후에 아이스 브레이킹 타임으로 임시 팀을 구성하고 함께 퀴즈를 맞추는 시간이 있었는데 생각보다 너무 잘 준비해주셔서 웃으며 모두와 친숙해질 수 있는 시간이었던 것 같다 ㅎㅎ

 

그리고 시작과 동시에 귀여운 컵과 티셔츠를 받아 기분이 좋았다!

이어서 self-pr 시간이 있었고 준비한대로 나를 어필하고자 했지만... 생각보다 잘 안됐던 것 같아 조금 아쉬움이 남았다.

 

이번 해커톤의 주제가 발표되었는데 바로 "제주도민의 삶을 바꾸는 생활 인프라 개선" 이라는 주제였다.

주로 구름톤 in Jeju에서는 이와 비슷한 주제로 제주도의 인프라, 삶의 개선 등에 대한 주제를 많이 다뤄온 것으로 보였다.

 

하지만 우리는 제주도민이 아는데 어떻게 제주도민의 삶을 바꾸는 것을 생각해볼 수 있을까? 라는 의문을 가지고 있을 때 구름톤에서는 여러가지 방향성과 레퍼런스를 제시해주며 해커톤을 진행함에 있어 필요한 내용들을 강의해주는 특강도 많은 도움이 되어 큰 무리 없이 아이디어를 떠올릴 수 있었다.

 

그리고 기획자 Matt님의 특강이 있었는데 다양한 AI 툴을 활용하여 퍼포먼스를 높이는 방식등에 대해 알게 되어 너무 좋았고, 기획자의 역량이 팀의 역량을 크게 좌우할 수도 있구나 라는 생각을 다시금 하게 될 만큼 애티튜드가 좋으셨다. 개발자에게도 기획 역량은 반드시 필요하다고 생각해온 만큼 매트님께 배운 것이 많아 기획자 특강에서 높은 만족감을 느낀 것 같다.

 

Day - 2

2일차에서는 1일차 일정에서 공개된 주제를 기반으로 One Page PPT를 만들어 각자의 기획 아이디어를 발표하고 어찌보면 2번째 self-pr이기도 한 발표시간이 있었다.

 

나는 어떠한 문제가 지속적으로 언급되면서 해결되지 않는 문제의 이유를

3가지로 정의하고 있었다.

 

1. 취합이 어려움

2. 사용하기 어려움( 민원 신고/접수 등 )

3. 시각적으로 이해하기 어려움

 

그래서 저는...

 

짜잔. 제가 만들어 발표한 ppt입니다... ㅎㅎ

 

그렇게 각자의 2차 self-pr을 진행하며 그 이후로 팀을 구성하는 시간에 챡챡챡 빠르게 팀들이 만들어져 갔다.

나 역시 팀을 구성하고 기획 아이디어에 대해 이야기 하며 프로젝트 방향성을 구성하고 있었다.

 

나는 나의 아이디어 자체는 매우 좋다고 생각했지만, 팀원들에게는 전달되지 못했고 채택되지 않았다...

 

최종 발표 / 심사위원 평가 시간에 나의 기획 아이디어를 심사위원으로 참여하신 스타트업 창업자 대표님께서 제안해주시는걸 보며 마음속에서는 "역시... 좋은 아이디어였어...!!!" 라는 메아리가 맴돌았고 나의 설득 능력이 부족하다는 것을 체감했다...😭

 

그렇게 모두 팀을 구성하고 플레이스 캠프로 이동하고 해커톤을 마저 진행하다. 구름톤에서 제공하는 숙소에 짐을 두고 왔다. 침대가 폭신하고 좋았다... 🥹물론 침대에 머물 수 있는 시간은 구름톤을 진행하는 동안 단 2시간 30분 뿐이었지만... 하하

 

그렇게 이어서 구름톤을 진행하다 비어파티 시간이 되어 저녁을구성원들과 함께 저녁을 먹으며 개발과 일상에 대해 소통하며 웃고 친해지는 시간이었다. 어느정도 시간이 흘러 멘토님과의 소통시간이 되었고 생각보다 알차고 재밌는 시간을 보내게 되어 좋았다.

 

그리고... 뷔페식 저녁 너무 맛있었다 ㅎㅎㅎ

 

그렇게 비어파티도 마무리되고 다시 해커톤에 집중하고... 그렇게 눈뜬채로 3일차가 되어버렸다🥹

 

Day - 3

팀원들과 프로젝트를 기획하고 개발하고 논의하며 점점 가까워지고 함께 웃으며 프로젝트를 진행할 수 있었다. 대화를 할 때 매번 편하게 논의할 수 있어서 "좋은 분들을 만나서 너무 다행이다"라는 생각이 들었고 팀원분들에게 너무 감사했다. ㅠㅠ

 

다들 수면시간이 4시간 미만임에도 불구하고 웃으며 나가서 커피 한 잔 하시죠! 하며 예쁘게 팀사진도 찍어서 좋았다.

 

그렇게 또 집중해서 어느덧 저녁이 되었고 오늘은 무조건 시간을 내서라도 제주도에 온 김에 바다를 봐야겠다는 다짐을 했다!

 

가는 길에 이름 모를 귀여운 백마도 만났다🐎

 

내가 찍어두고도 놀랐다... 이렇게 예쁘게 찍히다니! 안보러 왔으면 엄청 후회할 뻔했다 ㅎㅎ 그렇게 20분 정도 경치를 보며 이전에 정리하지 못했던 생각들도 정리하고 기분전환하고 다시 돌아와 MVP를 열심히 만들었다.

 

Day - 4

그렇게 2일차에 2시간, 3일차에 30분 총 2시간 30분 자면서 MVP를 완성하였고 팀원들과 짧은 기간에 최선을 다해 만들었다고 생각하니 매우 뿌듯했다! ㅎㅎ 그리고 우리 팀장님이 발표를 너무 잘해주셔서 너무 좋았다.

 

 

결과는 아쉽게도 수상은 하지 못했지만 좋은 사람들과 함께 웃으며 논의하고, 누구 하나 빠짐없이 집중하고 노력하는 모습이 더 소중하고 값진 경험이었던 것 같다.

 

정말 모두가 열정이 넘치고 의욕 가득하신분들 뿐이여서 나도 모르게 스며들어 더 열심히 하게되었던 것 같다.

 

발표가 끝나고 이야기를 종종 나누고 웃으며 대화했던 각 포지션 사람들과 연락처를 주고받으며 서로 사이드프로젝트 참여하거나 좋은 소식있으면 또 연락하자며 마무리하게 되었다.

 

해커톤에서 정말 많은 것을 배웠고 얻은 것 같다.

 

특히 "사람을 얻어간다." 라는 말이 가장 가깝게 다가왔다.

 

✍🏻마치며...

다양한 사람들과 소통하고 몰입하는 것 자체만으로도 애티튜드가 많이 달라지는 것을 체감했다. 그리고 스스로에게 말할 수 있게 되었다. "만약 내가 성장하지 못하고 있다면, 환경에 변화가 없는 것이다."

2025. 8. 10. 22:06

🤔찾아보게 된 계기

개발 커뮤니티를 돌아다니다 밈같은 것을 보다 우연히

DRY원칙을 준수하지 않는다 라는 이야기를 보고

갑자기 웬 드라이..? 하고 알아보게 되었다.

 

알고보니 3대 원칙 중 하나였고 뜻을 알고보니 밈을 다시

보고 피식하게 되었다.

 

😒KISS ( Keep It Simple, Stupid )

의미: 가능한 단순하게 유지하라. 복잡성을 피하고, 명확하고 이해하기 쉬운 코드를 작성하라.

 

목표:  코드의 가독성을 높이고 유지보수를 용이하게 하며, 버그 발생 가능성을 줄이는 것.

 

 

🥹YAGNI ( You Ain't Gonna Need It )

의미: 지금 당장 필요하지 않은 기능은 만들지 말라. 미래에 필요할 것 같다고 미리 구현하지 말고,

정말 필요할 때 구현하라.

 

목표: 개발 시간과 노력을 절약하고, 불필요한 기능으로 인한 복잡성을 줄이는 것.

 

 

😨DRY

의미: 코드 중복을 피하라.같은 기능을 하는 코드가 여러 곳에 반복되지 않도록 재사용 가능한

모듈이나 함수로 만들어 사용하라.

 

목표: 코드 유지보수를 용이하게 하고, 변경 사항 발생 시 모든 중복 코드를 일일이 수정해야

하는 번거로움을 줄이는 것.

 

 

✍🏻마치며...

소프트웨어 3대원칙을 알고보니 개발할 때 퀄리티( 가독성, 유지보수성 등 )를 높이고 개발자 특유의 "고민"을 많이 줄일 수 있도록 유도한 것 같았다. 단순히 밈 때문에 찾아보게 되었는데 생각보다 중요한 이야기였고 앞으로 개발할 때에도 이 3대원칙을 기반으로 코드를 작성해야겠다.