GROWTH LOG STORIES

함께 만든 성장을,
기록으로 연결합니다.

AI와 함께 배우고, 직접 만들고, 서로 나눈
그로스로그 멤버들의 생생한 성장 기록을 만나보세요.

성장 기록
5기+이어진 커뮤니티
HEALTHIO
PROJECT

사진 한 장으로 시작하는 건강 루틴, Health IO

대표 이야기 읽기

ALL STORIES

성장의 순간을
한곳에서 만나보세요.

프로젝트의 시작부터 모임에서 나눈 인사이트까지,
그로스로그가 함께 만든 기록을 모았습니다.
개의 성장 기록

성장일지

[GL 4기 4회차 ②] 손대기 전에 먼저 재는 것 — 효율을 정의하는 기준, 시간 복잡도, 크롤러 유지보수 🌱

GROWTH LOG🌱 2026. 8. 11. 00:09

안녕하세요! 그로스로그 입니다!😊 🌱

효율이 무엇인지 먼저 정하는 일, 1초에 1억 번이라는 기준으로 미리 계산해보는 일, 그리고 남이 짜둔 크롤러를 고치기 전에 구조부터 읽는 일. 4기 4회차 세 편입니다.

4기 4회차는 2025년 11월입니다. 세 편이 서로 다른 자리에 있는데, 공통점이 하나 있어요. 손을 대기 전에 먼저 무언가를 재고 시작한다는 것입니다.

 

01

🧭 [CS 공부 #4] 알고리즘: 가장 효율적인 길을 설계하는 사고의 기술

홍범영 님

이런 내용이에요
1회차에서 소개했던 열 편짜리 연재의 네 번째 글입니다. 예고한 목차대로 네트워크, 운영체제를 지나 알고리즘에 왔어요.
주장이 분명합니다. "효율은 '속도'가 아니라 '사고의 구조화'다."
알고리즘을 탐색 · 정렬 · 최적화 세 갈래로 나누고, 각각을 일상의 판단에 대응시킵니다.
그리고 이렇게 묻습니다. "무엇을 기준으로 효율을 정의할 것인가?"

1회차에서 이 연재의 첫 편을 소개했습니다. 그때 목차 열 개를 미리 걸어두고 출발했는데, 약속이 지켜지고 있습니다. 지난 편을 한 줄로 받아 다음 질문으로 넘어가는 도입부가 특히 좋아요.

"지난 글에서 우리는 운영체제(OS)를 통해 '한정된 자원을 어떻게 나눌 것인가'를 살펴봤습니다. 이제 그 다음 질문이 남았습니다. '그 자원을 가장 효율적으로 활용하는 법은 무엇일까?'"

연재가 목차의 나열이 아니라 질문의 연쇄로 이어지고 있습니다.

조금 더 들어가 보면

⚖️ 효율에는 정의가 먼저 필요합니다

이 글의 핵심 주장이 회의실 예시에 들어 있습니다. 회의실 세 개에 요청 다섯 개. 누구를 먼저 넣을 것인가.

저자는 여기서 두 가지 방식을 대비합니다.

· 먼저 온 순서대로 — 공정하지만 느립니다

· 중요도 순서대로 — 효율적이지만, 중요도가 낮은 일은 끝내 처리되지 않을 수 있습니다

두 번째의 단서가 중요합니다. 운영체제에서 이걸 기아 상태라고 부르는데, 우선순위가 낮은 작업이 계속 밀려 영원히 실행되지 않는 현상이에요. 실제 스케줄러는 이걸 막으려고 기다린 시간만큼 우선순위를 올려주는 장치를 따로 둡니다.

그래서 이 예시가 잘 고른 예시입니다. "효율적으로 하자"는 말만으로는 아무것도 정해지지 않는다는 걸 보여주거든요. 처리량을 최대로 할 것인지, 아무도 오래 기다리지 않게 할 것인지, 급한 일을 먼저 할 것인지에 따라 답이 전부 달라집니다.

"무엇을 기준으로 효율을 정의할 것인가"라는 질문이 알고리즘의 시작이라는 저자의 문장이 정확한 이유입니다.

🔍 절반을 버리는 용기의 조건

탐색 부분에서 이진 탐색을 "'가능성이 없는 길'을 지우는 용기 있는 지능" 이라고 표현했습니다. 좋은 비유인데, 한 가지를 덧붙이면 훨씬 실용적이 됩니다.

이진 탐색은 정렬돼 있어야만 쓸 수 있습니다.

절반을 버릴 수 있는 이유는 "가운데 값보다 크면 왼쪽에는 없다"는 걸 확신할 수 있기 때문이고, 그 확신은 정렬에서 옵니다. 정렬돼 있지 않으면 절반을 버리는 순간 답을 같이 버립니다.

그래서 실무에서는 계산이 하나 더 붙습니다. 정렬 자체가 대략 n log n이 드는 일이라, 한 번만 찾을 거면 그냥 처음부터 훑는 게 더 쌉니다. 정렬 비용이 탐색으로 아낀 것보다 크니까요.

이진 탐색이 이득이 되는 건 여러 번 찾을 때입니다. 한 번 정렬해두고 계속 찾는 구조요. 데이터베이스 인덱스가 정확히 이 계산 위에 서 있습니다. 넣을 때 조금 더 들이고, 찾을 때 크게 아끼는 것이죠.

저자의 비유에 조건을 붙이면 이렇게 됩니다. 절반을 버리는 용기는 미리 줄을 세워둔 사람에게만 허락됩니다.

🎯 그리디와 동적 계획법이 갈리는 자리

최적화 부분에서 저자는 그리디와 동적 계획법을 대비했습니다. "지금 당장 최선""과거의 결과를 저장해 반복 계산을 줄이는 것" 이요.

여기서 한 층 더 내려가면 실제로 유용한 구분이 나옵니다. 언제 그리디를 써도 되는가요.

그리디는 매 순간 가장 좋아 보이는 것을 고릅니다. 빠르고 코드도 짧아요. 문제는 부분의 최선이 전체의 최선이 아닐 수 있다는 것입니다.

동전 거스름돈이 유명한 예입니다. 100원·50원·10원짜리로 거슬러줄 때는 큰 것부터 집으면 항상 최소 개수가 나옵니다. 그런데 동전 단위가 1원·3원·4원이고 6원을 거슬러야 하면, 큰 것부터 집으면 4+1+1로 세 개가 됩니다. 실제 최선은 3+3으로 두 개고요.

같은 방법인데 동전 구성에 따라 맞기도 하고 틀리기도 합니다. 그래서 그리디를 쓰려면 "이 문제에서 부분 최선이 전체 최선으로 이어지는가"를 먼저 확인해야 합니다.

저자가 "빠르지만, 전체를 보지 못할 위험이 있습니다" 라고 적어둔 게 이 이야기입니다. 그리고 이 확인 과정이 바로 이 글이 계속 말하는 "기준을 세우는 일" 이고요.

📐 빅 오는 미래를 재는 자입니다

마지막 부분의 문장이 이 글에서 가장 실용적입니다.

"이건 단순한 수식이 아니라 '규모가 커질수록 어떤 사고가 병목이 되는가'를 보여주는 지표입니다."

빅 오가 지금의 속도를 재는 게 아니라 커졌을 때 어떻게 될지를 재는 것이라는 점을 정확히 짚었어요.

그래서 지금 느린 코드와 나중에 무너질 코드는 다릅니다. 데이터 100건에서는 O(n²)도 순식간입니다. 그런데 10,000건이 되면 100건의 만 배가 걸려요. 지금은 멀쩡한데 나중에 죽습니다.

반대로 상수 시간이 큰 알고리즘은 작은 데이터에서 느려 보여도 커질수록 유리해집니다. 그래서 실무 라이브러리들은 입력이 작으면 단순한 방법으로, 크면 복잡한 방법으로 갈아타는 식으로 구현돼 있는 경우가 많습니다.

"효율적인 사고는 곧 확장 가능한 사고" 라는 저자의 결론이 이 지점을 가리킵니다.

🗂️ 정렬 기준이 곧 우선순위입니다

정렬 부분에서 저자가 백로그 이야기를 꺼낸 게 흥미롭습니다. 개발 난이도로 볼지 사용자 가치로 볼지에 따라 결과가 달라진다는 대목이요.

이걸 조금 더 밀어보면 실무에서 쓰는 방식이 나옵니다. 기준이 하나면 반드시 이상해집니다.

난이도만 보고 쉬운 것부터 하면 쉬운 일만 계속 나오고 중요한 게 안 됩니다. 가치만 보고 하면 아무도 못 끝내는 큰 일만 남고요. 그래서 보통 두 축을 곱하거나 나눠서 봅니다. 가치를 비용으로 나눈 값이 큰 것부터 하는 식이요.

여기서 알고리즘 이야기가 실제로 이어집니다. 두 기준을 하나의 점수로 합치면 다시 한 줄로 세울 수 있다는 것이요. 정렬은 결국 비교할 수 있어야 성립하고, 비교할 수 있게 만드는 게 기준을 정하는 일입니다.

그리고 여기에도 함정이 있습니다. 점수를 매기는 순간 그 점수를 올리는 방향으로 일이 굴러갑니다. 측정 기준이 목표가 되면 그 기준은 더 이상 좋은 측정이 아니게 되고요. 그래서 실무에서는 점수로 순서를 정하되, 정한 뒤에 사람이 한 번 훑어보는 절차를 남겨둡니다.

"무엇을 기준으로 정렬할 것인가"라는 질문에는 "그 기준이 행동을 어떻게 바꾸는가"까지 따라옵니다.

🔁 재귀와 반복 — 같은 것을 두 가지로 적기

알고리즘을 배우면 곧 만나는 갈림길도 하나 짚어두겠습니다. 저자가 다룬 동적 계획법이 그 자리에 걸쳐 있거든요.

같은 계산을 위에서 아래로 쪼개 내려가며 할 수도 있고, 아래에서 위로 채워 올라가며 할 수도 있습니다. 결과는 같은데 성질이 다릅니다.

· 쪼개 내려가기 — 원래 문제의 모양이 그대로 코드에 남아 읽기 쉽습니다. 대신 호출이 깊어지면 스택이 쌓이고, 저장을 안 해두면 같은 계산을 수없이 반복합니다

· 채워 올라가기 — 반복문이라 스택 걱정이 없고 대개 빠릅니다. 대신 "왜 이 순서로 채우는가"가 코드에 안 드러나서 읽기 어렵습니다

저자가 말한 저장해두고 다시 안 계산하는 것이 첫 번째 방식을 쓸 만하게 만들어주는 장치입니다. 그게 없으면 같은 값을 지수적으로 반복해서 구하게 되거든요.

둘 다 "이미 한 계산을 다시 하지 않는다"는 하나의 아이디어입니다. 8회차에서 리액트의 메모이제이션 이야기가 나오는데, 뿌리가 같은 발상이에요.

📝 원문에서는 이렇게 씁니다

코드 없이, 기준을 세우는 일이 곧 알고리즘이라는 주장을 반복합니다.

"결국 알고리즘은 이렇게 묻습니다. '무엇을 기준으로 효율을 정의할 것인가?' 이 질문을 세우는 순간, 이미 알고리즘은 시작된 것입니다."

정렬 부분에서는 이렇게 적었습니다.

"중요한 건 무엇을 기준으로 정렬할 것인가 입니다. 백로그를 '개발 난이도'로 볼지, '사용자 가치'로 볼지에 따라 프로젝트의 효율은 완전히 달라집니다."

정렬 알고리즘 이야기를 하다가 정렬 기준 이야기로 넘어갑니다. 이 연재가 계속하는 동작이에요. 기술 개념을 판단의 언어로 바꿔놓는 것이요.

큐레이터 노트

1회차에서 걸어둔 목차가 실제로 이어지고 있는 연재라 뽑았습니다. 「시리즈로 이어진다」가 약속이 아니라 결과로 확인되는 자리예요.

이번 편에서 특히 좋았던 건 효율에 정의가 먼저 필요하다는 주장입니다. 회의실 다섯 개 요청 예시에서 "공정하지만 느리다"와 "효율적이지만 누군가는 영영 밀린다"를 나란히 놓은 대목이요. 알고리즘 글에서 잘 안 나오는 각도입니다.

바로 다음 글에서 정다은 님이 같은 주제를 숫자로 다룹니다.

원문 읽으러 가기  ↗hongby.tistory.com/14
 

02

⏱️ 코딩 테스트의 핵심, 시간 복잡도 이해하기

정다은 님

이런 내용이에요
시간 복잡도를 코딩 테스트를 통과하기 위한 실전 도구로 정리한 글입니다.
기준선을 하나 박아둡니다. "1초 ≈ 1억 번(=10⁸)의 연산" 이요.
그 기준으로 실제 계산을 해 보입니다. n = 10⁵일 때 O(n²)는 10¹⁰번이라 시간 초과, O(n log n)이면 10⁶번이라 통과.
빅-오메가 · 빅-세타 · 빅-오를 나누고, 코딩 테스트에서는 왜 빅-오만 보면 되는지까지 짚습니다.

앞 글이 "효율을 어떻게 정의할 것인가"를 물었다면, 이 글은 그 정의를 숫자로 바꿔 씁니다.

두 글을 나란히 놓으면 재미있어요. 같은 회차에서 한 사람은 알고리즘을 사고의 틀로 보고, 한 사람은 통과 기준으로 봅니다. 둘 다 맞고, 둘 다 필요합니다.

조금 더 들어가 보면

🧮 "1초에 1억 번"이 왜 쓸모 있는가

이 글의 값어치는 이 숫자 하나에 거의 다 들어 있습니다.

시간 복잡도를 배우면 대개 O(n²)O(n log n)보다 나쁘다는 것까지는 압니다. 그런데 지금 이 문제에서 어느 쪽을 써야 하는지는 답이 안 나와요. 둘 중 뭐가 나은지가 아니라 되느냐 안 되느냐를 알아야 하니까요.

"1초에 대략 1억 번"이라는 기준선이 그 다리를 놓습니다. 이제 곱셈만 하면 됩니다.

입력 크기 O(n²) O(n log n) O(n)
n = 1,000 10⁶ · 여유 10⁴ · 여유 10³ · 여유
n = 100,000 10¹⁰ · 초과 약 1.7×10⁶ · 여유 10⁵ · 여유
n = 1,000,000 10¹² · 초과 약 2×10⁷ · 통과 10⁶ · 여유

문제를 읽자마자 이 표를 머릿속에서 돌리면, 코드를 쓰기 전에 방향이 정해집니다. 저자가 마지막 팁으로 "문제를 풀기 전에 대략적인 시간 계산을 해본다"고 적은 게 이것이고요.

거꾸로도 쓸 수 있습니다. 문제에 적힌 입력 크기 제한이 사실은 힌트예요. n이 20 이하면 지수 시간을 써도 된다는 뜻이고, n이 10⁶이면 정렬 한 번이 한계라는 뜻입니다. 출제자가 의도한 풀이의 복잡도를 제한 조건이 알려주는 셈입니다.

📊 세 표기법 중 왜 빅-오만 쓰는가

빅-오메가, 빅-세타, 빅-오를 나눠 설명한 다음 저자는 이렇게 정리합니다.

"테스트 케이스는 대부분 가장 복잡한 입력까지 포함되기 때문에, 시간 복잡도를 평가할 때는 항상 빅-오(O)를 기준으로 계산해야 한다."

이유가 정확합니다. 그리고 한 발 더 들어가면 이건 코딩 테스트만의 이야기가 아닙니다.

평균이 좋아도 최악이 나쁜 알고리즘은 공격당할 수 있습니다. 퀵 정렬이 대표적이에요. 평균은 n log n인데 기준값을 잘못 잡으면 까지 갑니다. 정렬 기준을 예측할 수 있는 공격자가 최악을 유도하는 입력을 계속 보내면 서비스가 멈춥니다. 해시 테이블도 마찬가지고요. 평균은 상수 시간인데 충돌이 몰리면 선형이 됩니다.

그래서 실제 서비스에서도 최악을 봐야 하는 자리가 있습니다. 외부 입력을 받는 곳이요. 저자가 "실제 개발에서는 평균적인 성능도 중요하지만"이라고 여지를 둔 게 정확한데, 바깥에서 들어오는 값을 다룰 때는 최악이 기준이라는 조건을 붙이면 더 선명해집니다.

🔬 빅 오가 감추는 것들

이 도구의 한계도 같이 알아두면 좋습니다. 빅 오는 상수와 계수를 버립니다.

O(n)이라고 적힌 두 알고리즘이 실제로는 열 배 차이가 날 수 있어요. 한 줄당 하는 일이 다르니까요. 그래서 이런 일들이 생깁니다.

· 작은 입력에서는 복잡도가 큰 쪽이 더 빠릅니다 — 표준 라이브러리의 정렬이 작은 구간에서 삽입 정렬로 갈아타는 이유입니다

· 메모리 접근 패턴이 복잡도를 뒤집습니다 — 연결 리스트 순회와 배열 순회는 둘 다 O(n)인데 실제 속도는 크게 차이 납니다. 배열은 메모리에 붙어 있어 캐시가 잘 듣거든요

· O(1)이 항상 빠른 건 아닙니다 — 해시를 계산하는 비용이 짧은 배열을 훑는 것보다 클 수 있습니다

그래서 빅 오는 "커졌을 때 어떻게 되는가"에 대한 답이지, "지금 어느 쪽이 빠른가"에 대한 답이 아닙니다. 홍범영 님 글의 표현을 빌리면, 규모가 커질 때의 병목을 보는 지표인 것이죠.

🧾 외워둘 목록의 실제 쓰임

저자가 "자주 등장하는 알고리즘의 복잡도를 암기한다"고 적은 것도 실용적입니다. 다만 외우는 목적이 시험 대비만은 아니에요.

복잡도를 외워두면 코드를 읽을 때 비용이 보입니다. 반복문 안에 list.index()가 들어 있으면 그건 이중 반복문이고, 반복문 안에서 문자열을 계속 이어붙이면 매번 새로 만드는 겁니다. 겉보기에는 한 줄인데 실제로는 O(n)인 연산들이요.

"이 한 줄이 몇 번인가"를 아는 것. 그게 복잡도를 외우는 실제 이득입니다.

🧊 공간 복잡도도 같이 봐야 합니다

이 글이 시간에 집중한 만큼, 짝이 되는 것도 짚어두겠습니다. 메모리요.

코딩 테스트에는 시간 제한과 함께 메모리 제한도 붙습니다. 그리고 시간을 줄이려고 쓴 방법이 메모리를 늘리는 경우가 아주 흔해요.

· 미리 계산해두기 — 결과를 표로 만들어두면 조회는 빨라지는데 그 표를 들고 있어야 합니다

· 집합으로 바꾸기 — 목록에서 찾는 대신 집합에 넣어두면 훨씬 빠른데, 원본만큼의 공간이 더 듭니다

· 재귀 — 코드는 짧아지는데 호출이 깊어지면 스택이 쌓입니다

시간을 사려면 공간으로 지불하는 구조입니다. 2회차에서 장욱일 님이 도입부에 적었던 그 맞교환이 여기서도 나옵니다.

그래서 문제를 볼 때 입력 크기와 함께 메모리 제한도 같이 계산하는 게 맞습니다. n이 10⁶인데 이차원 표를 만들려 한다면 그 표만으로 제한을 넘기거든요. 재기 전에 무엇을 잴지 정하는 것에는 시간뿐 아니라 공간도 들어갑니다.

🚪 상수는 버려도 되고 안 되고

빅 오가 상수를 버린다고 했는데, 그 판단이 언제 안전하고 언제 위험한지도 나눠볼 만합니다.

안전한 경우 — 복잡도의 급이 다를 때입니다. n log n는 n이 조금만 커져도 상수 차이로는 못 뒤집습니다. 그래서 급이 다르면 상수를 무시하고 급으로만 골라도 됩니다.

위험한 경우 — 급이 같을 때입니다. 둘 다 O(n)인데 한쪽이 안에서 문자열을 계속 새로 만들고 있다면 실제로는 몇 배 차이가 납니다. 이때는 복잡도로 안 갈리고 실제로 재봐야 합니다.

코딩 테스트에서는 대개 급으로 갈립니다. 문제가 그렇게 설계돼 있어요. 그런데 실무에서는 급은 같은데 느린 상황이 훨씬 흔합니다. 그래서 실무에서는 복잡도 계산보다 실제 시간을 재보는 일이 먼저가 되고요.

저자가 정리한 도구가 언제 통하는지까지 알면 그 도구를 정확히 쓰게 됩니다.

📏 익숙해지면 눈으로 세게 됩니다

마지막으로 이 감각을 기르는 방법 하나를 덧붙이겠습니다. 저자가 "대략적인 시간 계산을 해본다"고 적은 그 습관이요.

처음에는 종이에 적어야 합니다. 그런데 몇 번 반복하면 코드를 보면서 바로 세게 됩니다.

· 반복문이 하나면 n

· 반복문 안에 반복문이면

· 반씩 줄여가면 log n

· 정렬이 한 번 들어가면 n log n

실무에서 마주치는 대부분이 여기까지입니다. 그리고 이 감각이 생기면 코드 리뷰에서도 보입니다. 다른 사람의 반복문 안에 조회가 들어 있는 걸 보고 "이거 목록 크기만큼 도는데요"라고 말할 수 있게 되죠.

시험을 위해 배우지만 남는 건 읽는 눈입니다. 저자가 "이런 감각을 익히면 시간 초과를 미리 방지할 수 있다"고 쓴 게 그 이야기예요.

🧮 자료구조를 고르는 일이 곧 복잡도를 고르는 일입니다

한 가지만 더 붙이겠습니다. 복잡도를 바꾸는 가장 쉬운 방법이 알고리즘이 아니라 자료구조를 바꾸는 것이라는 점이요.

목록에서 어떤 값이 있는지 확인하는 코드를 생각해봅시다. 배열이나 리스트에 담아두고 찾으면 처음부터 훑으니 O(n)입니다. 그런데 같은 값들을 집합에 넣어두면 평균 O(1)이에요.

코드는 거의 안 바뀌는데 급이 달라집니다. 그리고 이게 반복문 안에 있으면 전체가 O(n²)에서 O(n)으로 떨어져요.

비슷한 짝이 몇 개 더 있습니다.

· 찾기가 잦다 → 집합이나 사전

· 양쪽 끝에서 넣고 빼기가 잦다 → 덱

· 항상 가장 작은 것만 꺼낸다 → 우선순위 큐

· 정렬된 상태를 유지해야 한다 → 정렬된 자료구조

저자가 "자주 등장하는 알고리즘의 복잡도를 암기한다"고 한 목록에 자료구조별 연산 비용을 같이 넣어두면 문제를 볼 때 선택지가 훨씬 빨리 좁혀집니다.

📝 원문에서는 이렇게 씁니다

기준을 먼저 박고 계산을 보여줍니다.

"💡 1초 ≈ 1억 번(=10⁸)의 연산"

"입력 크기 n = 10⁵인 문제에서 알고리즘의 시간 복잡도가 O(n²)라면 약 10¹⁰번의 연산이 필요하다. → 1초 안에 끝나지 않는다."

"하지만 O(n log n) 알고리즘이라면 약 10⁶번 정도의 연산으로 충분하다. → 1초 안에 통과 가능하다."

그리고 팁의 첫 줄이 이 글 전체를 요약합니다.

"입력 크기 제한을 먼저 확인한다."

먼저 재고 나서 고른다. 이번 회차의 주제가 여기 그대로 있습니다.

큐레이터 노트

개념을 판단 기준으로 바꿔 쓴 글이라 뽑았습니다. 시간 복잡도 설명은 많은데, "1초 ≈ 1억 번"이라는 기준선을 박고 실제 곱셈까지 보여주는 글은 덜 흔해요. 읽고 나서 바로 쓸 수 있습니다.

세 표기법을 나열만 하지 않고 "그중 왜 빅-오만 보면 되는지" 를 이유와 함께 적은 것도 좋았습니다. 나열은 검색하면 나오지만 선택 이유는 잘 안 나오거든요.

원문 읽으러 가기  ↗devika.tistory.com/127
 

03

🕷️ 크롤링 코드 수리하기

노현주 님

노현주 님 원문에 실린 이미지
이런 내용이에요
처음 접해본 크롤링 코드를 유지보수한 기록입니다. 새로 짠 게 아니라 이미 있던 것을 고쳤어요.
Scrapy와 Selenium을 이어주는 미들웨어가 무슨 일을 대신해주는지부터 정리합니다.
그리고 막힌 지점 둘을 적었습니다. 뷰포트 밖 요소를 클릭할 수 없는 문제XML에 특수문자가 들어가 깨지는 문제요.
목적을 처음에 밝힙니다. "진행하며 알게된 것들을 복기 목적으로 정리."

이 회차의 앞 두 편이 재는 이야기였다면, 이 글은 실제로 남의 코드 앞에 앉은 기록입니다.

그리고 이런 기록이 드뭅니다. 블로그에는 새로 만든 이야기가 많고 이미 있는 걸 고친 이야기는 적어요. 실무 시간의 대부분은 후자인데도요.

조금 더 들어가 보면

🔧 미들웨어가 대신해주는 것을 먼저 확인했습니다

이 글이 코드를 고치기 전에 한 일이 있습니다. 지금 쓰고 있는 도구가 무엇을 대신해주고 있는지 정리한 것이요.

저자가 정리한 대비가 이렇습니다.

· 미들웨어를 쓸 때 — 요청을 처리할 때 브라우저 드라이버를 띄우고, 주소를 열고, 끝나면 닫아줍니다. 자바스크립트로 그려지는 화면도 프레임워크 안에서 그대로 다룰 수 있습니다

· 직접 쓸 때 — 코드 안에서 드라이버를 손수 만들고, 다 쓰면 손수 닫아야 합니다

"닫아야 한다" 가 핵심입니다. 브라우저 드라이버는 실제 프로세스라, 안 닫으면 계속 쌓입니다. 크롤러처럼 수백 번 반복하는 코드에서는 이게 곧 메모리 고갈로 이어져요.

그래서 남의 코드를 고칠 때 "이건 누가 정리해주고 있나" 를 먼저 보는 게 맞습니다. 미들웨어가 해주고 있는데 안에서 또 만들면 두 배로 뜨고, 아무도 안 해주는데 안 만들면 안 돌아갑니다.

1회차의 JPA 이야기와 같은 자리입니다. 프레임워크가 대신 해주는 자리를 모르면 예측할 수 없다는 것이요.

🖱️ 안 보이면 못 누른다

첫 번째 문제가 흥미롭습니다.

"Selenium은 화면에 보이지 않는 요소(뷰포트 밖)에 대한 상호작용(클릭)이 불안정하거나 불가능할 수 있음"

DOM에는 분명히 있는데 클릭이 안 되는 상황입니다. 처음 겪으면 코드가 요소를 못 찾은 줄 알고 선택자를 계속 고치게 되는데, 찾기는 찾았고 누를 수가 없는 것이에요.

이유는 자동화 도구가 실제 사용자처럼 동작하도록 만들어져 있기 때문입니다. 사람은 화면 밖의 버튼을 누를 수 없으니 도구도 안 누릅니다. 요소가 다른 것에 가려져 있어도 마찬가지고요.

저자가 쓴 해법이 정석입니다. 먼저 화면 안으로 스크롤해 넣고 그다음에 누르는 것이요.

driver.execute_script("arguments[0].scrollIntoView...", _expand_btn)

여기서 한 가지 더 덧붙이면, 스크롤 직후 바로 클릭하면 또 실패할 수 있습니다. 애니메이션이 끝나기 전이면 요소가 아직 움직이는 중이거든요. 그래서 실무에서는 "클릭 가능한 상태가 될 때까지 기다린다"는 조건을 붙입니다. 고정된 시간만큼 자는 것보다 상태를 조건으로 거는 편이 안정적이에요.

⏳ 언제 다 그려졌는지 아는 문제

저자가 짚은 또 하나가 이 문제의 뿌리를 건드립니다.

"driver.page_source를 반복적으로 확인하여 JavaScript 동작으로 인한 DOM 변경이 멈출 때까지 기다려야 함"

동적 페이지 자동화의 근본 문제입니다. 옛날 웹은 서버가 완성된 문서를 보내주니 도착하면 끝이었는데, 지금은 도착한 뒤에도 화면이 계속 바뀝니다. 데이터를 더 불러오고, 요소를 새로 그리고요.

그래서 "다 됐다"는 신호가 없습니다. 저자가 화면 내용이 안 바뀔 때까지 반복해 확인하는 방식을 쓴 게 그래서입니다. 실무에서 흔히 쓰는 방법 세 가지가 있는데, 순서대로 나은 편입니다.

· 일정 시간 기다린다 — 가장 쉽고 가장 나쁩니다. 짧으면 실패하고 길면 느려집니다

· 바뀌지 않을 때까지 본다 — 저자가 쓴 방법입니다. 범용적이지만 판정 기준을 잘 잡아야 합니다

· 특정 요소가 나타날 때까지 기다린다 — 가장 안정적입니다. "결과 목록의 첫 항목이 뜨면 됐다"처럼 끝을 명시적으로 정의하는 것이죠

세 번째가 가능하면 세 번째가 낫습니다. 기다림의 끝을 시간이 아니라 조건으로 정의하는 것이 자동화 코드를 안정적으로 만드는 거의 유일한 방법이에요.

⚠️ 데이터에 문법 기호가 섞이는 문제

두 번째 문제는 성격이 완전히 다릅니다.

"XML 출력 시 데이터 내부에 <> 기호가 포함되어 XML 문법 오류를 일으킬 가능성이 있다면, 반드시 CDATA 섹션으로 감싸야 한다."

수집한 본문 안에 태그처럼 보이는 문자가 들어 있으면, 그걸 그대로 XML에 쓸 때 파서가 태그로 읽어버립니다. 데이터와 문법이 섞이는 것이죠.

이 문제의 형태를 기억해두면 여기저기서 다시 만납니다.

자리 데이터가 섞여 들어가면
XML·HTML 태그로 해석돼 문서가 깨집니다
SQL 쿼리로 해석됩니다 — SQL 인젝션
웹 페이지 스크립트로 해석됩니다 — XSS
셸 명령 명령으로 해석됩니다

전부 같은 문제입니다. 값으로 넣은 것이 코드로 읽히는 것이요. 그래서 해법도 같은 계열입니다. 값이 값으로만 읽히도록 감싸거나, 자리를 분리해 넘기는 것. CDATA로 감싸는 것, SQL에서 값을 직접 문자열에 넣지 않고 자리표시자로 넘기는 것, 화면에 출력할 때 특수문자를 바꿔주는 것 전부요.

저자가 덧붙인 주의가 실전적입니다. "XML 생성 라이브러리가 자동으로 CDATA 선언을 인코딩하지 않도록 주의." 감싸는 표시 자체가 또 한 번 변환되면 소용이 없다는 것이죠. 두 번 처리해서 망가지는 경우인데, 겪어보지 않으면 안 나오는 지적입니다.

📝 원문에서는 이렇게 씁니다

고치기 전에 구조부터 확인합니다.

xpath -> /html/body/div[1]/section/div/div/div[2]/div/section/section[1]/h2

그리고 수집 로직에는 이렇게 걸어뒀습니다.

self.total_count += 1
self.custom_logger.info(f"기사 '{book_id}' 수집 요청 - '{response.url}'")
error_flag = False

세는 것과 남기는 것과 표시하는 것이 나란히 있습니다. 크롤러는 수백 번 도는 동안 사람이 안 보고 있으니, 나중에 무슨 일이 있었는지는 이 세 줄에만 남습니다. 유지보수를 해본 사람이 다음 사람을 위해 남겨두는 자리예요.

🧱 크롤러가 자주 깨지는 이유

이 기록이 "수리"에서 시작한다는 게 사실 크롤링이라는 작업의 성격을 그대로 보여줍니다. 크롤러는 반드시 깨집니다.

이유가 구조적이에요. 크롤러는 남이 만든 화면의 생김새에 기대어 동작합니다. 저자가 적어둔 저 긴 경로가 그 의존의 실체입니다.

/html/body/div[1]/section/div/div/div[2]/div/section/section[1]/h2

이 경로는 상대편이 화면을 조금만 손봐도 무효가 됩니다. 감싸는 태그 하나가 늘어나면 전부 어긋나요. 그쪽은 우리 크롤러가 있는 줄도 모르고 고칠 테고요.

그래서 실무에서 쓰는 요령이 몇 개 있습니다.

· 위치보다 의미로 찾습니다 — 몇 번째 자식인지보다, 특정 표시나 이름이 붙은 요소를 찾는 쪽이 덜 깨집니다

· 짧게 잡습니다 — 뿌리부터 훑는 긴 경로보다, 가까운 기준점에서 시작하는 짧은 경로가 안전합니다

· 깨진 걸 빨리 압니다 — 수집 건수가 갑자기 0이 되거나 절반이 되면 알림이 오게 해둡니다

세 번째가 가장 중요합니다. 크롤러가 깨지는 건 막을 수 없으니, 깨진 걸 언제 아느냐가 실제 관리 포인트예요. 저자가 넣어둔 건수 세기와 로그가 그 자리에 있습니다.

🤝 그리고 상대편도 사람이 만든 서비스입니다

기술 이야기 옆에 하나 덧붙일 게 있습니다. 크롤링은 남의 서버에 부담을 주는 작업이라는 점이요.

브라우저를 띄워 실제로 화면을 그리게 하는 방식은 특히 무겁습니다. 요청 한 번이 이미지·스크립트까지 전부 불러오니까요. 그래서 사람이 쓸 때와 비슷하거나 그 이상의 부하가 갑니다.

기본으로 지키면 좋은 것들이 있습니다.

· 요청 사이에 간격을 둡니다 — 쉬지 않고 때리면 상대 서버에서 차단당하고, 차단당하면 작업도 못 합니다

· 수집 정책을 확인합니다 — 사이트가 밝혀둔 규칙이 있으면 따르는 게 맞습니다

· 필요한 만큼만 가져옵니다 — 전부 긁어두고 나중에 고르는 것보다, 필요한 범위를 정해두는 쪽이 서로에게 낫습니다

결국 오래 도는 크롤러를 만드는 방법이기도 합니다. 차단당하지 않는 게 안정성의 큰 부분이거든요.

큐레이터 노트

남이 짜둔 코드를 처음 열어보고 고친 기록이라 뽑았습니다. 새로 만든 이야기는 많은데 고친 이야기는 드물고, 실무에서 더 자주 하는 건 후자입니다.

특히 막힌 지점 두 개를 원인과 함께 적어둔 것이 좋았어요. "안 되더라"가 아니라 "화면 밖이라 못 누른다", "데이터에 문법 기호가 섞인다"까지 갔습니다. 「실패한 시도가 남아 있다」와 「왜 그런지까지 갔다」가 함께 붙는 자리예요.

노현주 님은 4기 전 회차에 걸쳐 기록을 남겼습니다. 6회차부터 다시 만나게 됩니다.

원문 읽으러 가기  ↗anyway03.tistory.com/6

💡 세 편을 겹쳐 읽으며

고르기 전에 잽니다

세 편의 첫 동작이 같습니다.

정다은 님은 "입력 크기 제한을 먼저 확인한다" 로 시작합니다. 홍범영 님은 "무엇을 기준으로 효율을 정의할 것인가" 를 먼저 묻습니다. 노현주 님은 코드를 고치기 전에 미들웨어가 무엇을 대신해주는지부터 정리했습니다.

셋 다 손대기 전에 무언가를 재고 시작했습니다. 그리고 그 재는 동작이 그다음 선택을 거의 결정합니다. n이 10⁵이면 이중 반복문은 이미 탈락이고, 효율의 기준이 "아무도 오래 기다리지 않기"면 우선순위 방식은 이미 탈락입니다.

고를 게 많아 보이는 문제가, 재고 나면 대개 하나둘로 줄어듭니다.

같은 개념이 두 층에서 나타납니다

홍범영 님과 정다은 님이 같은 주제를 다른 높이에서 씁니다.

한 사람은 알고리즘을 사고의 틀로 봅니다. 탐색·정렬·최적화가 일상의 판단과 어떻게 닮았는지요. 다른 한 사람은 통과 기준으로 봅니다. 1억 번을 넘느냐 마느냐요.

둘을 같이 읽어야 완성됩니다. 기준만 알면 왜 그런지 모르고, 사고만 알면 지금 이 문제를 못 풉니다. 우연히 같은 회차에 두 편이 들어온 건데, 겹쳐 놓으니 한 편씩 읽을 때보다 남는 게 많았어요.

재는 도구에도 한계가 있습니다

그런데 세 편 모두 재는 도구의 한계도 같이 안고 있습니다.

빅 오는 상수를 버려서 작은 입력에서는 틀린 답을 줍니다. 효율의 기준은 정하는 순간 다른 기준을 포기하는 것이고요. 크롤러에서 "화면이 안 바뀌면 다 된 것"이라는 판정도 사실 추측입니다. 진짜로 끝났는지는 알 수 없어요.

재는 일이 정답을 주지는 않습니다. 고를 수 있게 해줄 뿐입니다. 그래서 잰 다음에도 확인이 필요하고, 세 편 다 그 확인 과정을 함께 적었습니다.

이번 회차에서 가져갈 것

1. 문제를 읽으면 입력 크기부터 보기.

"1초 ≈ 1억 번"을 기준으로 곱해보면 쓸 수 있는 복잡도가 정해집니다. 제한 조건이 사실은 풀이 방향에 대한 힌트입니다.

2. "효율적으로 하자"를 그대로 두지 않기.

처리량인지, 최대 대기시간인지, 우선순위인지 정해야 답이 나옵니다. 정하지 않으면 나중에 "누군가는 영영 밀리는" 구조가 조용히 만들어집니다.

3. 이진 탐색은 여러 번 찾을 때만 이득.

정렬 비용이 따로 들기 때문에 한 번 찾을 거면 그냥 훑는 게 쌉니다. 인덱스를 거는 판단도 같은 계산 위에 있습니다.

4. 남의 코드를 고치기 전에 "누가 정리해주고 있나" 확인하기.

드라이버·커넥션·파일 핸들처럼 닫아야 하는 것들이 프레임워크에서 처리되는지 코드에서 처리되는지 먼저 보세요. 중복해서 만들거나 아무도 안 닫는 상태가 여기서 생깁니다.

5. 기다림은 시간이 아니라 조건으로 정의하기.

"3초 대기" 대신 "이 요소가 나타날 때까지"로 쓰세요. 짧으면 실패하고 길면 느린 문제가 사라집니다.


4기 4회차는 2025년 11월이었습니다.

연재 중인 CS 공부와 코딩 테스트 준비와 실무 코드 유지보수라는 서로 다른 자리에서, 세 분이 나란히 손대기 전에 먼저 재는 이야기를 남겼습니다. 재는 대상은 달랐지만 순서는 같았어요.

성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.

좋은 글 남겨주신 홍범영 님, 정다은 님, 노현주 님께 감사드립니다. 그리고 4기를 함께 채워주신 모든 멤버분들께도요! 😊

다음 큐레이션으로 다시 찾아오겠습니다! 🌱

- 에디터 · 성장일지 큐레이터 -


※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.

WRITE YOUR GROWTH

오늘의 배움을
내일의 성장으로.

기록은 나를 돌아보게 하고, 공유는 우리를 연결합니다.
당신의 다음 성장 이야기도 그로스로그에서 시작해보세요.

커뮤니티 둘러보기