
서버에 sleep을 심으려 했던 이야기 — 지연 로딩(Lazy Load) 체감 검증과 '구현하지 않기'라는 선택
요약
데이터 증가로 인한 화면 로딩 지연 문제를 해결하기 위해 지연 로딩(Lazy Load) 도입을 검토한 과정입니다. 서버에 인위적인 지연(sleep)을 심어 검증하려던 계획 대신, IaC를 활용한 독립적인 검증 환경 구축과 AI를 통한 구조적 질문으로 더 나은 해법을 찾아낸 경험을 다룹니다.
핵심 포인트
- 지연 로딩 도입을 위한 독립적인 검증 환경(dev04) 구축
- IaC를 활용하여 네트워크부터 Redis까지 50개 리소스를 신속히 복제
- 환경과 서비스의 분리 설계를 통한 효율적인 검증 리소스 관리
- AI를 활용한 구조적 질문으로 불필요한 구현 리스크 제거
서론
감사하게도 fixU는 고객이 계속해서 늘어나고 있습니다. 그에 따라 관리하는 데이터량도 증가하여, 관리 화면의 일부에서 표시될 때까지 잠시 기다려야 하는 화면이 나타나기 시작했습니다.
이러한 화면에 대해서는 지금까지도 측정을 통해 개선 기회를 특정하고, DB 인덱스 튜닝, 쿼리 개선, 캐싱(Caching)을 지속적으로 진행해 왔습니다. 효과는 컸으며, 어떤 화면에서는 10초를 넘겼던 표시 시간이 2초 미만으로 단축되었습니다. 하지만 쿼리 측면의 개선을 쌓아가다 보면, "화면 한 분량의 집계를 모두 마친 후 동기적(Synchronously)으로 그린다"라는 구조 자체의 한계도 보이게 됩니다. 전부를 기다린 후에 그려야 하므로, 가장 무거운 집계가 화면 전체의 속도를 결정해 버리기 때문입니다.
그래서 다음 단계로 채택한 것이, 렌더링과 데이터 취득을 분리하는 지연 로딩(Lazy Load)입니다. 페이지의 외곽 틀은 즉시 반환하고, 무거운 집계는 스켈레톤(Skeleton, 로딩 중의 플레이스홀더 표시)으로 둔 채, 비동기(Ajax)로 나중에 가져와서 채웁니다. 이와 함께 "응답 시간의 p90이 1000ms를 초과하는 블록은 비동기화한다"라는 판단 기준도 사내 규칙으로 마련했습니다.
다만, 이 기사는 지연 로딩 본체의 구현기가 아닙니다. 기록해 두고 싶은 것은 그 직전에 일어난 "검증 환경의 즉석 구축"과 "체감 수단을 둘러싼 의사결정", 특히 한때 구현할 뻔했던 『서버에 sleep을 심는』 안을 최종적으로 단 한 줄도 쓰지 않고 버린 이야기입니다.
먼저, 이 기사에서 가져가셨으면 하는 핵심을 한 문장으로 적어둡니다.
떠오른 구현안을 바로 만들지 않고 "더 좋은 방법은 없을까"를 AI에게 구조적으로 질문하면, 구현 제로·리스크 제로의 대체 해법에 도달할 수 있는 경우가 있습니다. 그리고 그것을 뒷받침한 것은, 필요할 때 즉석에서 세울 수 있는 검증 환경이었습니다.
1. 대규모 개수는 공유 dev에 둘 수 없다 — 검증 전용 환경을 즉석에서 구축하기
지연 로딩은 프론트엔드부터 컨트롤러까지를 가로지르는, 제법 대규모인 변경입니다. 평소의 공유 개발 환경(develop 브랜치에 직결되어 있으며, 여러 개발이 공존하는 곳)에 그대로 올리면, 진행 중인 다른 릴리스에 휘말리거나 반대로 휘말리게 할 우려가 있습니다. 차분하게 거동을 관찰하고 싶은 변경일수록 주변의 노이즈로부터 격리된 장소에서 시험하고 싶은 법입니다.
그래서 개발 브랜치를 그대로 배포할 수 있는 검증 전용 환경을 새로 구축하기로 했습니다(사내에서는 편의상 dev04라고 부릅니다).
여기서 크게 효과를 본 것이 최근 진행하던 IaC(Infrastructure as Code, 인프라 구성을 코드로 관리하는 노력)였습니다. 환경을 "환경 디렉터리 + 공유 모듈"이라는 구조로 관리하고 있었기 때문에, 기존 환경의 디렉터리를 복사하여 CIDR 등을 교체하는 것만으로, 단 한 개의 PR을 통해 네트워크부터 로드 밸런서(Load Balancer), Redis까지 약 50개의 리소스로 구성된 환경 일체가 구축되었습니다.
특히 효과적이었던 것은 이 구성에서의 "환경"과 "서비스"의 분리입니다. 이 설계에서는 환경은 기반(네트워크나 로드 밸런서 등)만을 제공합니다. 앱의 컨테이너 서비스는 각 서비스의 리포지토리를 해당 환경에 배포해야 비로소 생겨납니다. 즉 "이번 검증에 필요한 2개 서비스만 배포한다"라고 결정하면, 기동할 서비스의 선택이 자연스럽게 성립합니다. "이 서비스는 세울 것인가/세우지 않을 것인가"를 제어하는 플래그를 굳이 구현할 필요 없이, 배포 여부가 그대로 선택이 되어 비용도 최소화되었습니다.
데이터 관련 부분은 과감히 결정했습니다. DB는 기존 개발 환경과 스키마(Schema)를 공유했으므로 데이터 준비는 제로입니다. 반면 Redis만은 독립된 노드를 구축했습니다. 큐(Queue), 세션(Session), 캐시(Cache)와 같은 실행 시의 상태는 공유해 버리면 워커(Worker)가 서로의 작업을 가로채는 등의 혼선이 발생하기 때문입니다. "정적으로 공유해도 좋은 것(스키마)"과 "실행 시에 분리해야 할 것(상태)"을 나누어 생각한 것입니다.
직접 해보며 얻은 가장 큰 발견은, 환경을 구축한다는 행위의 심리적 장벽이 생각보다 훨씬 낮아져 있었다는 점이었습니다. 이전 같았으면 "전용 환경을 구축할 바에는 공유 dev에서 어떻게든 궁리하자"라고 생각했을 상황에서, 망설임 없이 "그렇다면 전용 환경을 구축하자"라고 선택할 수 있게 되었습니다. 수단의 비용이 낮아지면 선택할 수 있는 선택지 자체가 늘어난다는 것을 실감했습니다.
장 끝의 증류: 환경을 구축하는 비용이 낮아지면, "별도 환경에서 안전하게 시험한다"가 현실적인 선택지에 들어온다. IaC의 배당은 코드의 깔끔함 이전에 속도 그 자체였다.
2. "너무 빨라서 체감할 수 없다" 문제 — 검증 수단 4안의 비교
환경이 갖춰졌으므로, 곧바로 지연 로딩 (Lazy Load)을 적용하여 움직여 보았습니다. 그런데 여기서 예상치 못한 벽에 부딪힙니다.
검증 환경은 데이터가 적기 때문에, 비동기 (Asynchronous) 취득이 수십 ms 내에 반환되어 버립니다. 결과적으로 스켈레톤 (Skeleton)에서 실제 데이터로의 전환이 순식간에 끝나버려 눈으로 쫓을 수 없습니다. "로딩 중 표시에서 값이 부드럽게 들어오는" 지연 로딩에서 가장 확인하고 싶었던 경험을, 애초에 체감할 수 없는 것입니다. 너무 빨라서 판정할 수 없다는 아이러니한 상황이었습니다.
그래서 "어떻게 지연을 재현하고, 체감하여 판정할 것인가"를 수단 수준에서 나열해 보았습니다. 비교한 것은 다음 4가지 안입니다.
| 안 | 내용 | 판정 |
|---|---|---|
| A | 서버 측에 디버그용 환경 변수를 준비하여 임의의 지연 (sleep)을 심는다 | 언뜻 확실해 보임. 하지만 멈춤 (→ 3장) |
| ... |
B1과 C를 버리기로 한 판단은 리드 엔지니어의 지적이 결정적이었습니다. B1에 대해서는 "이번에는 데이터량이 매우 적기 때문에, 대역폭 (Bandwidth)을 제한해도 체감할 수 있는 차이가 되지 않는다. 효과가 있는 것은 페이지 전체가 일률적으로 느려지는 것인데, 그것은 지연 로딩의 모습과는 별개의 것이다". C에 대해서는 "스테이징 (Staging)에 도달할 때쯤에는 개발 환경에서의 체감 및 판정은 이미 끝나 있어야 한다. 순서가 반대다".
듣고 보니 당연한 이야기였지만, 안을 나열하고 있을 때는 "확실해 보이는가", "구현이 쉬운가"에만 눈이 가 있었고, 애초에 지금 이 공정에서 무엇을 체감하고 싶은가라는 핵심을 놓치고 있었습니다.
장 끝의 증류: 검증 수단은 "무엇을 체감하고 싶은가"로부터 역산하여 선택한다. 이번에 체감하고 싶었던 것은 "외곽은 빠르고 데이터만 느린" 지연 로딩 고유의 모습이었으며, 대역폭 제한(전체가 일률적으로 느려짐)이나 스테이징(판정 이후의 공정)으로는 그 모습을 만들어낼 수 없었습니다.
3. sleep 안에서 멈춤 — 사전 검시 (Pre-mortem)가 보여준 운영 환경 혼입 리스크
여기서부터가 이 글에서 가장 쓰고 싶었던 부분입니다.
사실, 처음의 생각은 안 A, 즉 저(인간)의 발안이었습니다. "디버그용 환경 변수를 준비해서 거기에 임의의 지연을 심을 수 있게 하면, 임의의 밀리초 단위로 스켈레톤의 모습을 확실히 체감할 수 있다". 언뜻 보기에는 가장 확실하고 타당한 안처럼 보였습니다.
다만, 바로 구현으로 넘어가기 전에 "더 좋은 방법이 있을지도 모른다"며 한 번 멈춰 서서, AI에게 **태스크 착수 전의 메타 검토 프로토콜 (Meta-検討 Protocol)**로 검토를 의뢰했습니다. 이 프로토콜은 이전에 다른 글에서 소개한 task-kickoff로, 업계 베스트 프랙티스 (Best Practice)의 정리 → 암묵적 전제의 언어화 → 사전 검시 (Pre-mortem. "만약 미래에 이 판단이 사고로 이어진다면, 원인은 무엇이었을까"를 미리 파악하는 기법) → 대안의 강제 열거,와 같은 관점을 AI 스스로 자발적으로 개시하게 하는 틀입니다 (자세한 내용은 링크를 참조해 주세요).
이 중 사전 검시를 통해, sleep 안의 다음 3가지 리스크가 도출되었습니다.
리스크 1: 설정의 환경 간 복사로 인해 운영 환경에 전파될 수 있음. 환경 변수 설정은 환경을 늘릴 때마다 기존 환경에서 복사해 오는 운영 방식이 실제로 존재합니다. 실제로 이번 검증 환경을 만든 당일에도 기존 환경에서 설정을 복사하여 구축했습니다. 즉, 디버그용 지연 변수가 향후 어떤 환경 복사 흐름을 타고 운영 환경의 설정에 섞여 들어갈 경로가 운영상 구조적으로 존재하게 됩니다.
리스크 2: 동기 처리 모델에서의 sleep은 워커 (Worker)를 점유함. PHP-FPM과 같은 동기 처리 모델에서는 sleep으로 대기하는 동안에도 워커 1개를 계속 점유합니다. 만약 오설정과 부하 타이밍이 겹친다면, 성능을 좋게 하기 위해 넣은 기능이 오히려 성능 문제를 일으킬 수 있습니다. 성능 개선 기능이 성능 사고를 초래한다는, 웃지 못할 아이러니입니다.
리스크 3: 검증 전용 코드 패스 (Code Path)가 운영 환경에 상주함. 운영 코드에 "검증만을 위한 분기"가 계속 남게 됩니다. 환경 가드 (Environment Guard, 운영 환경에서는 절대 발화하지 않도록 하는 조건)를 달아둔다 하더라도, "그 가드가 정말로 올바른가"를 앞으로 모든 리뷰에서 계속 확인해야 합니다. 설정한 순간뿐만 아니라 확인 비용이 영구적으로 발생하게 됩니다.
세 가지를 나열했을 때, 손익의 비대칭이 명확해졌습니다. sleep 안으로 얻을 수 있는 것(일시적인 체감)은 나중에 다른 수단으로 완전히 대체할 수 있습니다. 반면 잃을 수 있는 것(운영 코드 혼입 리스크와 그것을 감시하는 비용)은 영구적입니다. 일시적인 편익을 위해 영구적인 리스크를 떠안는 것은 수지가 맞지 않습니다. 이렇게 해서 안 A는 단 한 줄도 구현하지 않은 채 반려되었습니다.
재미있는 점은, 안 A를 제안한 것도, 그것을 반려하기로 판단한 것도 바로 나 자신이었다는 점입니다. 아이디어 자체는 나쁘지 않았습니다. 다만 "바로 만들기" 전에 한 호흡 쉬며 구조적으로 다시 질문해 본 결과, 그 아이디어가 가진 좋지 않은 흐름이 드러났습니다. 인간의 아이디어는 출발점으로서 충분히 가치가 있지만, 그 가치는 "멈춰서 검산하는" 과정과 세트로 맞물릴 때 비로소 발휘된다고 생각합니다.
장 끝의 증류: 인간의 아이디어는 출발점으로서 가치가 있다. 단, "바로 만들기" 전에, 얻는 것이 일시적이고 잃는 것이 영구적이지는 않은지 구조적으로 질문하라. 사전 검사(Pre-mortem)는 그 질문을 형식화해 준다.
4. 채택한 JS 주입 방식 — 북마크릿(Bookmarklet) 1 클릭의 유사 리플레이(Pseudo-replay)
sleep 안을 반려하는 대신 채택한 것이 안 B2, 브라우저 측에서 JavaScript를 주입하는 방식이었습니다.
하는 일은 간단합니다. 브라우저의 페이지 컨텍스트(Page Context)에서 다음 3단계를 실행합니다. ① 이미 표시되어 있는 값을 일단 스켈레톤(Skeleton) 표시로 되돌린다 → ② 지정한 밀리초(ms)만큼 기다린다 (이 부분이 서버 처리 지연의 유사 재현에 해당합니다) → ③ 실제 데이터 취득 엔드포인트(JSON)를 다시 호출하여 값을 채운다. 말하자면, 지연 로딩(Lazy Load)의 모습을 수동으로 "유사 리플레이"하는 것입니다.
실행 방법은 두 가지로 준비했습니다. 개발자 도구의 콘솔에 붙여넣어 실행하거나, 혹은 북마크릿(Bookmarklet)으로 만들어 1 클릭으로 실행하는 것입니다. 후자의 경우, 지연시킬 밀리초 수는 실행할 때마다 다이얼로그로 지정할 수 있으므로, 1.6초든 8초든 그 자리에서 자유롭게 바꿔가며 몇 번이고 체감할 수 있습니다.
일반화된 코드 예시를 올려둡니다 (속성명이나 경로는 일반화되어 있습니다).
javascript:(async () => {
const delayMs = Number(prompt('지연 밀리초', '1600')) || 1600;
const card = document.getElementById('summary-card');
...
이 방식의 장점은 3장에서 언급한 리스크와 정반대되는 지점에 있습니다. 서버 측의 코드 변경이 전혀 없으므로, 운영 환경(Production)으로 혼입될 경로가 구조적으로 존재하지 않습니다. 지연 값은 자유롭게 바꿔가며 몇 번이고 테스트할 수 있고, 브라우저 내에서 모든 것이 완결됩니다. 3장에서 "얻을 수 있는 것(일시적인 체감)은 다른 수단으로 완전히 대체 가능하다"라고 쓴 것은 바로 이 방식을 의미한 것이었습니다.
다만, 제약 사항도 솔직하게 적어둡니다. 이것은 어디까지나 "모습의 재현"일 뿐이며, 화면 전환 직후에 스켈레톤이 보이는 것과 같은, 최초 로딩(First Load)의 맥락 그 자체를 재현하는 것은 아닙니다. 진정한 최초 체감은 최종적으로 운영 환경에 준하는 데이터를 가진 스테이징(Staging) 환경에서 확인해야 합니다. 이 방식은 개발 초기 단계에서 모습을 여러 번 다듬기 위한 발판(Scaffolding)이지, 최종 확인을 대체하는 것이 아니라는 위치를 가집니다.
장 끝의 증류: 검증용 지연은 서버 내부에서 만들지 말고, 외부(브라우저)에서 주입하라. 앱 본체를 더럽히지 않는 검증은 운영 환경으로의 혼입 리스크를 차단함과 동시에, 검증 그 자체의 신뢰성도 높여준다.
5. 요약
지연 로딩 본체는 이제부터 본격적으로 검증해 나갈 단계입니다. 다만 그 전 단계로서 "개발 브랜치를 그대로 테스트할 수 있는 검증 전용 환경"과 "모습을 몇 번이고 체감할 수 있는 주입 수단"이라는 두 가지 발판이 마련되었습니다. 그리고 그 과정에서, 한때 구현하려 했던 sleep 안을 단 한 줄도 쓰지 않고 내려놓았습니다.
돌이켜보면 핵심은 "아이디어가 떠오르면 바로 만든다"는 단계 이전에, 딱 한 번 멈춰 서는 과정을 끼워 넣은 것이었습니다. 구현하지 않는 것이 최선책이 되는 상황은 분명히 존재하며, 그곳에 도달하기 위해서는 아이디어를 구조적으로 다시 질문해 줄 상대가 필요합니다. AI와의 검토는 그 재질문 과정에 매우 적합했습니다. 그리고 대체안을 안심하고 선택할 수 있었던 것은, 언제든 즉석에서 세울 수 있는 검증 환경이라는 발판이 있었기 때문입니다.
fixU는 아직 발전 중인 서비스입니다. AI를 비롯한 기술의 힘을 최대한 활용하며, 고객님께 더 좋은 서비스를 제공하기 위해 매일 정진하겠습니다.
관련 기사
- AI는 알고 있는데 사용하지 않는다 — 설계 태스크의 "task-kickoff" 프로토콜로 잠재 능력을 끌어내기 — 본 기사 3장에서 사용한, 태스크 착수 전의 메타 검토 프로토콜에 대한 상세 내용.
- 대증요법의 반대 · AI에게 훈계를 들은 이야기 — 위화감 센서의 양방향화 — 인간의 아이디어와 AI의 시점을 서로 보정하는 협업 이야기. 본 기사와 같은 구도의 다른 사례.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기