안녕하세요! 그로스로그 입니다!😊 🌱
게임 프레임이 어디서 느려지는지 재는 법, 루프가 세 개인 코드가 하나인 코드보다 빠른 이유, 그리고 SQL에서 같은 결과를 내는 두 문장 중 고르기. 3기 2회차 세 편입니다.
3기 2회차, 2025년 4월 초입니다. 이번 회차에는 속도를 판단하는 이야기 세 편이 모였어요. 재서 알아내고, 세어서 알아내고, 겪어서 알아냅니다.
01
🎮 프로파일링과 최적화 작업
허해수 님
언리얼 엔진의
stat 커맨드를 정리한 노트입니다. 화면에 성능 수치를 띄우는 명령어들이요.커맨드마다 무엇을 보여주는지를 표로 적었고, 화면 캡처를 붙였습니다.
그중
Stat UNIT에는 항목별 설명을 따로 붙였어요. 어느 수치가 크면 어디가 병목인지까지 적혀 있습니다.뒷부분은 Mesh LOD 조절 방법이고, 마지막에 모르는 것을 모른다고 적어뒀어요.
조금 더 들어가 보면
📏 먼저 재는 것부터 배운다
이 노트는 최적화 기법이 아니라 재는 도구로 시작합니다.
목록을 보면 성격이 갈려요. FPS는 프레임만 보여주고, Stat GPU는 GPU가 지금 뭘 하고 있는지 목록을 보여줍니다. Stat Engine은 삼각형·버텍스·드로우콜 수를 보여주고요.
그리고 Stat UNIT에 이런 설명이 붙었습니다.
This is great stat command to start with because it helps developers focus their profiling work.
"시작하기 좋은 커맨드"라는 공식 문서의 문장을 그대로 가져왔습니다.
왜 시작점일까요. 다른 커맨드는 한 영역을 자세히 보여주는데, Stat UNIT은 어느 영역을 봐야 하는지를 알려주기 때문입니다.
🔀 숫자가 어디에 가까운가
Stat UNIT의 항목 설명이 이 노트에서 가장 실용적입니다.
Game | 프레임 시간이 게임 시간에 가까운 경우 게임 스레드 때문에 게임 퍼포먼스에 병목 구간이 발생하는 것일 수 있음
Draw | 프레임 시간이 드로 시간에 가까운 경우 렌더링 스레드 때문에 병목 구간이 발생하는 것일 수 있음
GPU | 비디오 카드가 씬을 렌더링할 때 얼마나 오래 걸리는지 측정함
"~에 가까운 경우"라는 판단 방법이 핵심입니다.
절대값을 보는 게 아니라 비교를 하는 겁니다. Frame이 20ms인데 Game도 20ms라면, 한 프레임에 드는 시간 대부분을 게임 로직이 쓰고 있다는 뜻이죠. Draw가 3ms밖에 안 된다면 렌더링은 문제가 아닙니다.
이게 왜 중요하냐면, 엉뚱한 데를 고치는 걸 막아주기 때문입니다. 게임이 느릴 때 흔히 그래픽 품질부터 낮추는데, 병목이 게임 스레드에 있으면 그래픽을 아무리 낮춰도 그대로거든요.
그리고 첫 줄의 기준값도 유용합니다. 60프레임이면 16.66ms, 30프레임이면 33.33ms. 프레임을 초로 나눈 값이에요. 목표가 60프레임이라면 한 프레임에 쓸 수 있는 예산이 16.66ms라는 뜻입니다.
성능 작업을 예산 개념으로 보면 판단이 쉬워집니다. 지금 20ms가 걸린다면 3.34ms를 어디선가 깎아야 하는 것이고, 어디서 깎을지는 Game·Draw·GPU 중 가장 큰 데서 찾으면 되니까요.
🔺 삼각형을 줄이는 손잡이
뒷부분은 실제로 줄이는 작업입니다. Mesh의 LOD 설정이요.
LOD는 멀리 있는 물체를 단순한 모델로 바꿔 그리는 기법입니다. 화면에서 작게 보이는 나무를 굳이 5만 개 삼각형으로 그릴 필요가 없으니까요.
노트에 적힌 순서가 이렇습니다. Nanite 지원을 끄고, LOD 개수를 정하고, 각 단계의 Triangle 수를 설정하는 것요.
새로 생긴 LOD만큼 Triangles 수 설정이 가능하다.
그리고 이런 문장이 붙어 있습니다.
"잘 모르겠다"를 그대로 적었습니다.
Nanite는 언리얼 5의 기능인데, LOD를 사람이 정하지 않고 엔진이 알아서 처리합니다. 그래서 손잡이가 비활성화되는 거예요. 자동과 수동 중 어느 쪽이 나은지는 상황에 따라 다르고요.
몰라서 적지 않는 대신 모른다고 적어두면 나중에 그 자리를 다시 찾아올 수 있습니다. 앞의 회차에서도 여러 번 본 습관이에요.
📝 원문에서는 이렇게 씁니다
이 노트에는 설명 문장이 거의 없습니다. 표와 캡처가 대부분이에요.
stat 커맨드마다 한 줄 설명을 달고, 실제로 화면에 띄운 캡처를 붙이는 식입니다. 그리고 맨 위에 언리얼 공식 문서 링크를 하나 걸어뒀어요.
공식 문서가 있는데 왜 따로 정리했을까요.
문서에는 커맨드가 훨씬 많이 나옵니다. 이 노트는 그중 자기가 쓸 것만 골라 담았어요. D3D11RHI, FPS, GPUParticles, Stat GPU, Streaming, SceneRendering, Engine, UNIT까지요.
그리고 캡처가 있다는 게 차이입니다. 문서의 설명만 읽으면 화면에 뭐가 뜨는지 상상이 안 되거든요. 직접 띄워본 화면이 붙어 있으면 다음에 골라 쓸 때 훨씬 빠릅니다.
큐레이터 노트
최적화가 아니라 계측을 정리한 게 이 글을 고른 이유입니다.
성능 이야기는 대개 "이렇게 하면 빨라진다"로 갑니다. 그런데 그 앞에 "지금 뭐가 느린가"가 있어야 해요. 그걸 모르면 빨라진다는 기법을 다 적용해놓고도 그대로일 수 있습니다.
Stat UNIT이 그 첫 단추입니다. Frame·Game·Draw·GPU 네 숫자를 나란히 놓고 어느 게 프레임 시간에 가까운지 보는 것. 딱 그 한 가지 판단으로 어디를 팔지가 정해집니다.
그리고 커맨드 목록을 캡처와 함께 남긴 것. 이건 다음에 프로파일링을 시작할 때 바로 꺼낼 수 있는 형태입니다.
02
🔢 빈도수 세기 패턴 Frequency Counters
조민재 님
자바스크립트 객체로 빈도를 세는 문제 해결 패턴을 정리한 글입니다.
예제 두 개를 쓰는데, 두 번째가 이 글의 핵심이에요. 같은 문제를 두 가지 방법으로 풀고, 시간복잡도를 비교합니다.
하나는
O(n²), 하나는 O(n)이요.그리고 겉보기와 실제가 다르다는 걸 짚습니다. 루프가 세 개인 코드가 루프가 하나인 코드보다 빠릅니다.
조금 더 들어가 보면
🔤 애너그램을 세어서 푼다
첫 예제가 애너그램 판별입니다. 두 문자열이 같은 글자를 같은 개수만큼 갖고 있는지 보는 문제요.
풀이 순서를 이렇게 적었습니다.
두 번째 문자열에서 첫 번째 문자열의 문자가 나타날 때마다, 객체에 저장된 빈도수를 하나씩 차감합니다.
쌓았다가 빼는 구조입니다. 다 빼고 나서 0이 아닌 게 남으면 두 문자열이 다른 거예요.
이 방식이 좋은 이유는 두 문자열을 서로 비교하지 않는다는 데 있습니다. 각각을 한 번씩만 훑으면 되죠. 서로 비교하려 들면 첫 문자열의 글자마다 두 번째 문자열을 뒤져야 하니까요.
⏱️ 루프가 겹치는가
두 번째 예제가 이 글의 진짜 주제입니다. 문제는 이거예요.
그리고 패턴을 안 쓴 풀이를 먼저 보여줍니다. indexOf로 찾아서 splice로 지우는 방식이요.
시간복잡도를 고려하지 않는다면 말이죠.
"~않는다면 말이죠"로 한 박자 끊은 게 좋습니다.
그리고 문제를 짚습니다.
indexOf 메서드의 기능은 전체 배열을 잠재적으로 반복하는 것입니다.결국, 위 함수는
for 루프 안에서 indexOf 루프가 중첩되는 함수인 것이죠.이게 초보자가 놓치는 자리를 정확히 겨눴습니다.
코드에는 for가 한 번밖에 안 보입니다. 그런데 그 안의 indexOf가 실은 루프예요. 눈에 안 보이는 중첩입니다.
splice도 마찬가지입니다. 배열 중간에서 하나를 빼면 뒤의 요소들을 전부 앞으로 당겨야 하거든요. 이것도 안에서 도는 루프고요.
메서드 하나가 몇 번 도는지를 알아야 시간복잡도가 보인다는 것. 이건 문법만 배워서는 안 생기는 감각입니다.
🧮 루프가 많은 쪽이 빠르다
빈도수 세기로 다시 푼 코드가 이렇습니다.
let frequencyCounter1 = {};
let frequencyCounter2 = {};
for (let val of arr1) {
frequencyCounter1[val] = (frequencyCounter1[val] || 0) + 1;
}
for (let val of arr2) {
frequencyCounter2[val] = (frequencyCounter2[val] || 0) + 1;
}
for (let key in frequencyCounter1) {
if (!(key**2 in frequencyCounter2)) return false;
if (frequencyCounter2[key**2] !== frequencyCounter1[key]) return false;
}
return true;
for가 세 개인데 이쪽이 빠릅니다. 글이 그 점을 짚었어요.
O(n)이 중첩 루프가 사용된 O(n²)보다 훨씬 효율적입니다.개수가 아니라 겹침이 문제라는 것입니다.
for가 세 번 나란히 있으면 n + n + n = 3n이고, 빅오에서는 계수를 떼니 O(n)입니다. 반면 하나가 다른 하나 안에 들어가면 n × n = n²이 되고요.
배열이 1,000개라면 3,000번과 1,000,000번의 차이입니다. 333배요.
그리고 (frequencyCounter1[val] || 0) + 1도 눈여겨볼 만합니다. 없는 키를 꺼내면 undefined가 나오는데, || 0으로 받아서 첫 등장을 0으로 시작하게 한 거예요. 키가 있는지 먼저 확인하는 if문 없이 한 줄로 처리했습니다.
📝 원문에서는 이렇게 씁니다
글의 순서가 잘 짜여 있습니다. 패턴 소개 → 쉬운 예제 → 어려운 예제(안 쓴 풀이) → 시간복잡도 지적 → 패턴 적용 → 비교요.
특히 세 번째 단계가 중요합니다. 안 쓴 풀이를 먼저, 그리고 충분히 자세히 보여줬어요. 코드도 붙이고 동작 설명도 다섯 줄로 적었습니다.
그래야 비교가 됩니다. 좋은 방법만 보여주면 "그렇구나"로 끝나는데, 흔히 쓰는 방법을 먼저 보여주고 나서 문제를 짚으면 자기 코드가 떠오르거든요.
첫머리의 패턴 설명도 정확합니다.
언제 쓰는지를 세 가지로 나눠 적었습니다. 패턴을 배울 때 정작 어려운 게 "이 문제가 그 패턴인지 알아보는 것"인데, 이런 목록이 그 판단을 도와줍니다.
큐레이터 노트
보이지 않는 루프를 짚어낸 게 이 글을 고른 이유입니다.
시간복잡도를 배울 때 첫 관문이 for 개수를 세는 것입니다. 그런데 그다음 관문이 있어요. indexOf, includes, splice, sort 같은 내장 메서드 안에도 루프가 있다는 것요.
이걸 모르면 코드가 깔끔해 보이는데 느립니다. 그리고 왜 느린지 알 수가 없어요. for는 하나뿐이니까요.
이 글은 정확히 그 지점을 예제로 보여줍니다. 그리고 "루프가 세 개인데 더 빠르다"는 결론까지 데려가요. 코드 길이와 성능이 별개라는 걸 한 번에 보여주는 예입니다.
앞의 허해수 님 글이 재서 병목을 찾는 이야기라면, 이 글은 읽어서 미리 아는 이야기입니다. 둘 다 필요한 능력이고요.
03
🗃️ SQL 기초적인 공부 정리
송지원 님
SQL 기초를 항목별로 정리한 노트입니다.
SELECT·ORDER BY·WHERE·LIKE·집계 함수·문자 함수까지요.문법 정리인데 성능 이야기가 곳곳에 섞여 있습니다.
IN이 OR보다 빠르다거나, LIKE 앞에 %를 쓰면 인덱스를 못 쓴다거나요.그리고 혼잣말이 그대로 남아 있어요. "전자가 나아서 전자를 쓰는거겠지?" 같은 문장이요.
항목마다 직접 만든 연습 문제도 붙어 있습니다.
조금 더 들어가 보면
⚡ 문법 노트에 속도 이야기가 섞인다
기초 정리인데 성능 언급이 세 군데 나옵니다. 이게 이 노트의 특징이에요.
첫째, IN과 OR입니다.
WHERE 카테고리='신발' OR 카테고리='가전' OR 카테고리='식품'을 WHERE 카테고리 IN ('신발','가전','식품')으로 줄이는 것이죠. 짧아지기도 하지만 DB가 실행 계획을 짜기도 쉬워집니다.
둘째, LIKE와 인덱스입니다.
%문자 앞에 쓰면 성능 이슈가 있을 수 있다. index 활용을 못 할 수 있음이게 실무에서 자주 걸리는 함정이에요. LIKE '소파%'는 인덱스를 탈 수 있는데 LIKE '%소파%'는 못 탑니다.
이유가 인덱스의 구조에 있습니다. 인덱스는 앞에서부터 정렬된 목록이거든요. 전화번호부처럼요. "김"으로 시작하는 이름은 금방 찾는데, "중간에 '수'가 들어간 이름"은 처음부터 다 봐야 합니다.
%를 앞에 붙이는 순간 그 상황이 됩니다. 시작이 정해지지 않으니 인덱스가 소용없어요.
셋째, MAX와 LIMIT입니다.
MAX()를 쓰는게 너무 오래걸릴 수 있음SELECT * FROM card ORDER BY 사용금액 DESC LIMIT 1;정렬해서 첫 줄만 가져오는 것으로 최댓값을 구하는 방법입니다. 해당 컬럼에 인덱스가 있으면 이미 정렬된 상태라 끝에서 하나만 집으면 되니까요.
기초 문법 노트에 이런 게 함께 적혀 있는 건 드뭅니다. 보통 문법 다 배우고 나서 따로 배우는 내용이거든요.
💭 혼잣말이 남아 있다
LIKE 항목에 이런 줄이 있습니다.
not 칼럼명 like, 칼럼명 not like 둘 다 되는 거 같은데 전자가 나아서 전자를 쓰는거겠지?"~겠지?"로 끝납니다. 정리 노트에 이런 문장이 남아 있는 게 재미있어요.
둘 다 문법상 되고 결과도 같습니다. 스타일 차이에 가까운데, 조건이 여러 개 붙기 시작하면 어디까지가 부정인지 헷갈리는 정도가 달라집니다.
여기서 눈여겨볼 건 답보다 태도입니다. 두 가지가 다 되는 걸 발견했고, 왜 한쪽을 더 쓰는지는 아직 모르겠고, 그 상태를 그대로 적어뒀어요.
char 컬럼 주의사항도 같은 성격입니다.
char() 칼럼에 쓰려면 주의해야 함, char 칼럼은 공백도 문자 취급을 한다.char(10)에 '소파'를 넣으면 뒤에 공백 여덟 칸이 붙습니다. 그래서 LIKE '소파'로 찾으면 안 나와요. 실제로 한 번 데어봐야 알게 되는 것이고, 그걸 적어둔 겁니다.
🧩 결론을 다섯 줄로 묶는다
WHERE 항목이 끝나는 자리에 정리가 붙습니다.
SELECT FROM 뒤에 WHERE 조건식을 붙여서 필터링할 수 있고2) 조건식란엔
> < = != >= <= 전부 이용 가능하고3) 조건식 여러 개가 필요하면
AND OR 이런 걸로 이어붙일 수 있고4) 괄호로
AND OR 사용한 부분을 묶을 수도 있고5)
OR 조건식 여러 개 필요하면 IN () 사용해도 될 때가 있습니다.앞에서 예제로 흩어놨던 걸 한 덩어리로 모았습니다.
이 다섯 줄이 실은 WHERE 절의 전부입니다. 조건 하나 → 비교 연산자 → 여러 조건 → 묶기 → 축약이요. 쓸 수 있는 것이 늘어나는 순서로 배열되어 있어요.
집계 함수 쪽도 같은 구성입니다. 기본 → 응용1(AS) → 응용2(필터링 후 통계) → 응용3(DISTINCT)로요. 그리고 응용2에 이런 한 줄이 붙었습니다.
문법이 아니라 쓰임에 대한 이야기입니다. 전체 평균은 알아도 쓸 데가 없는데, VIP 등급의 평균은 판단에 쓰이니까요.
📝 원문에서는 이렇게 씁니다
항목마다 직접 만든 문제가 붙어 있습니다.
Q3. 연체횟수가 1회 이하인 사람은 몇 명일까요?
Q2. 위 컬럼에서 휴대폰 뒷자리 4글자만 출력하려면 어떻게 코드를 짜야할까요?
그리고 답을 바로 아래에 적었어요. 마지막 문제에는 답을 두 개 달았습니다.
SELECT RIGHT(번호, 4) from 테이블명 SELECT SUBSTR(번호, 10, 4) from 테이블명
같은 결과를 내는 두 가지 방법이죠. RIGHT는 뒤에서 세고, SUBSTR는 앞에서 셉니다.
문법 노트에서 답을 두 개 적는 건 흔치 않습니다. 그리고 이 노트의 성격과도 맞아요. IN과 OR, MAX와 LIMIT도 같은 결과를 내는 두 가지였으니까요.
DB 종류에 따른 차이도 챙겼습니다.
*Oracle Postgres에서는 || 사용해서 합쳐줘야 함.*Postgres는 OVERLAY()를 쓴다. / Oracle은 없어서 SUBSTR() 써서 직접 하면 된다.표준 SQL과 구현체별 차이를 구분해둔 것입니다. 문자열 함수는 특히 DB마다 다른 영역이라, 이런 메모가 옮겨 갈 때 쓰입니다.
큐레이터 노트
말투를 다듬지 않은 게 이 노트의 특징입니다.
"~하면 됨", "~쓰는거겠지?", "~하는게 좋습니다"가 섞여 있어요. 누구에게 보여주려고 문장을 고른 게 아니라 배우면서 그때그때 적은 것입니다.
그런데 그래서 남는 게 있습니다. 어디서 헷갈렸는지가 보이거든요. not like의 두 가지 위치, char 컬럼의 공백, %의 위치 같은 것들요. 다듬으면 지워졌을 자리입니다.
그리고 기초 문법을 배우는 단계에서 성능 이야기를 함께 적어둔 것. 인덱스가 뭔지 아직 안 배웠을 텐데도 "인덱스 활용을 못할 수 있음"을 메모해뒀어요. 지금은 못 쓰더라도 나중에 인덱스를 배울 때 이 줄이 다시 걸립니다.
💡 세 편을 겹쳐 읽으며
세 글의 자리가 다릅니다. 게임 엔진, 자바스크립트 알고리즘, SQL이요.
그런데 셋 다 같은 결과를 내는 여러 방법 중 하나를 고르는 이야기를 합니다.
아는 방법이 세 가지
무엇이 빠른지 아는 방법이 셋 다 다릅니다.
허해수 님은 재서 압니다. 화면에 수치를 띄우고, Frame과 Game과 Draw를 비교해서 병목을 찾아요.
조민재 님은 세어서 압니다. 돌려보지 않고 코드만 읽어도 루프가 겹치는지 보면 알 수 있죠.
송지원 님은 들어서 압니다. IN이 OR보다 빠르다거나, %를 앞에 쓰면 인덱스를 못 탄다거나 하는 걸 배우면서 함께 메모했어요.
세 방법은 대체재가 아니라 순서에 가깝습니다. 아는 것부터 피하고(들어서), 짤 때 미리 계산하고(세어서), 그래도 느리면 재보는 것(재서)요.
눈에 안 보이는 비용
두 글에 같은 이야기가 나옵니다. 겉으로 안 보이는 비용요.
조민재 님의 indexOf가 그렇습니다. 한 줄인데 안에서 배열 전체를 돕니다. 코드 길이와 실행 횟수가 따로 논다는 것이고요.
송지원 님의 LIKE '%소파%'도 같습니다. '소파%'와 글자 하나 차이인데 인덱스를 쓰느냐 마느냐가 갈립니다.
허해수 님의 삼각형 개수도 비슷해요. 모델을 배치하는 건 클릭 한 번인데, 그 안에 든 삼각형 수가 프레임 시간을 먹습니다.
셋 다 "쓰기는 쉬운데 비용은 큰" 자리입니다. 그래서 알고 있어야 하고, 몰랐다면 재봐야 알게 되죠.
두 가지를 나란히 적기
기록 방식에서도 공통점이 있습니다. 선택지를 하나만 적지 않았다는 것.
조민재 님은 안 쓸 풀이를 코드까지 붙여 먼저 보여줬습니다.
송지원 님은 마지막 문제에 답을 두 개 적었고, DB별 차이도 함께 남겼어요.
허해수 님은 Nanite를 켠 경우와 끈 경우를 둘 다 적었습니다. 어느 쪽이 나은지는 모르겠다는 말과 함께요.
고른 것만 적으면 왜 골랐는지가 안 남습니다. 두 개가 나란히 있어야 다음에 상황이 달라졌을 때 다른 쪽을 꺼낼 수 있고요.
이번 회차에서 가져갈 것
1. 고치기 전에 어디가 느린지부터 보기.
언리얼이라면 Stat UNIT으로 Frame·Game·Draw·GPU를 나란히 놓고 어느 게 프레임 시간에 가까운지 확인하세요. 병목이 아닌 데를 고치면 아무것도 안 바뀝니다.
2. 목표 프레임을 시간 예산으로 바꿔보기.
60프레임이면 한 프레임에 16.66ms입니다. "느리다"보다 "3ms 초과"가 훨씬 다루기 쉬운 문제입니다.
3. 내장 메서드 안의 루프를 세기.
indexOf·includes·splice는 한 줄이지만 배열을 훑습니다. for 안에 넣으면 중첩이고, 그 순간 O(n²)이 됩니다.
4. 루프 개수보다 겹침을 보기.
나란한 for 세 개는 O(n), 겹친 for 두 개는 O(n²)입니다. 코드가 길어 보여도 나란한 쪽이 빠릅니다.
5. LIKE 앞의 %를 조심하기.
'소파%'는 인덱스를 타지만 '%소파%'는 못 탑니다. 중간 검색이 꼭 필요하면 전문 검색을 따로 두는 편이 낫습니다.
3기 2회차는 2025년 4월 초였습니다. 기수가 자리를 잡아가던 무렵이에요.
게임 엔진과 알고리즘 강의와 SQL 첫 노트라는 서로 다른 자리에서, 세 분이 나란히 속도를 판단하는 법을 기록했습니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.
좋은 글 남겨주신 허해수 님, 조민재 님, 송지원 님께 감사드립니다. 그리고 3기를 함께 채워주신 모든 멤버분들께도요! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.