안녕하세요, 카피바라 개발자입니다. 🦫🍊 프론트엔드와 백엔드를 함께 공부하다 보면 "API를 호출한다", "DB에 저장한다", "배포한다"는 말을 한꺼번에 듣게 됩니다. 오늘은 회원가입 버튼을 누르는 한 번의 행동을 따라가며, 웹서비스가 함께 움직이는 데 필요한 용어를 제 말로 정리해 보았습니다. 어떤 문제를 해결하려 했는지 처음에는 화면을 만들면 서비스가 끝난 줄 알았습니다. 그런데 사용자가 입력한 이름을 저장하고, 다음에 다시 보여 주려면 화면 밖에서 일하는 여러 친구가 필요했습니다. 이 글의 목표는 용어를 외우는 것이 아니라, 브라우저에서 버튼을 누른 뒤 화면이 다시 바뀔 때까지 흐름을 연결해 보는 것입니다. 먼저 한 장으로 보는 흐름 사용자 → 프론트엔드 → API → 백엔드 → 데이터베이스 → 백엔드 → API → 프론트엔드 예를 들어 "귤 목록 불러오기" 버튼을 누르면 프론트엔드는 서버에 요청을 보내고, 백엔드는 데이터베이스에서 목록을 찾아 응답합니다. 프론트엔드는 그 응답을 화면에 그립니다. 1. 프론트엔드와 백엔드 프론트엔드(Frontend): 사용자가 직접 보는 부분 프론트엔드는 버튼, 입력창, 글 목록처럼 브라우저에 보이는 화면입니다. HTML은 뼈대, CSS는 꾸밈, JavaScript는 클릭했을 때의 동작을 맡는다고 생각하면 이해가 빨랐습니다. 백엔드(Backend): 화면 뒤에서 규칙을 처리하는 부분 백엔드는 "로그인한 사람만 글을 쓸 수 있다" 같은 규칙을 확인하고, 필요한 데이터를 꺼내거나 저장합니다. 사용자가 직접 보지는 않지만 서비스의 중요한 판단을 담당합니다. 프론트엔드는 식당의 메뉴판과 주문대, 백엔드는 주방에 비유할 수 있습니다. 메뉴판만 예쁘다고 음식이 나오지는 않고, 주방만 있어도 손님이 주문하기 어렵습니다. 2. 클라이언트와 서버 클라이언트(Client): 요청을 시작하는 쪽 보통은 사용자의 웹 브라우저가 클라이언트입니다. 앱도 서버에 정보를 요청하면 클라이언트가 될 수 있습니다. 서버(Server): 요청을 받아 답하는 쪽 서버는 요청을 받고 규칙에 따라 처리한 뒤 결과를 돌려줍니다. 서버라고 해서 꼭 거대한 컴퓨터 한 대를 뜻하지는 않습니다. 요청을 처리하도록 실행 중인 프로그램과 그 환경을 함께 떠올리면 됩니다. 3. HTTP와 HTTPS: 웹의 대화 규칙 HTTP(HyperText Transfer Protocol)는 브라우저와 서버가 정보를 주고받는 약속입니다. 클라이언트가 보내는 말을 요청(Request) , 서버가 돌려주는 말을 응답(Response) 이라고 합니다. HTTPS는 이 HTTP 통신에 TLS 암호화를 더한 형태입니다. 로그인 정보처럼 보호가 필요한 데이터도 있으므로 실제 서비스에서는 HTTPS를 기본으로 고려해야 합니다. 자주 보는 상태 코드는 이렇게 읽었습니다. 200 OK : 요청을 정상 처리했다. 201 Created : 새 데이터를 만들었다. 400 Bad Request : 보낸 요청 형식이나 값이 맞지 않는다. 401 Unauthorized : 인증 정보가 없거나 유효하지 않다. 404 Not Found : 찾는 주소나 데이터가 없다. 500 Internal Server Error : 서버 처리 중 예상하지 못한 문제가 생겼다. 4. API: 프로그램끼리 만나는 창구 API(Application Programming Interface)는 프로그램끼리 정해진 방식으로 정보를 주고받는 창구입니다. 프론트엔드가 데이터베이스에 직접 들어가지 않고, 백엔드의 API에게 부탁하는 이유는 권한과 규칙을 한곳에서 관리하기 위해서입니다. 아래는 프론트엔드에서 목록 API를 부르는 아주 짧은 예시입니다. const response = await fetch('/api/tangerines'); const data = await response.json(); console.log(data); 첫 줄의 fetch 는 브라우저가 /api/tangerines 주소로 요청을 보내는 함수입니다. await 는 응답이 올 때까지 다음 줄을 잠시 기다리게 합니다. 둘째 줄의 json() 은 서버가 보낸 JSON 형식의 글자를 JavaScript에서 다루기 쉬운 값으로 바꿉니다. 마지막 줄은 받은 데이터를 개발자 도구 콘솔에 확인하는 용도입니다. 5. JSON: 서로 알아듣기 쉬운 데이터 포장 JSON(JavaScript Object Notation)은 데이터를 키: 값 형태로 표현하는 텍스트 형식입니다. 언어 이름에 JavaScript가 들어가지만 Java, Python 등 다른 언어에서도 널리 씁니다. { "name": "한라봉", "stock": 12 } 여기서 name 과 stock 은 데이터의 이름(키)이고, 한라봉 과 12 는 값입니다. API를 만들 때는 어떤 키를 어떤 자료형으로 주고받을지 팀과 약속하는 일이 중요합니다. 6. 데이터베이스와 DBMS 데이터베이스(Database, DB)는 서비스가 오래 보관해야 하는 데이터를 저장하는 공간입니다. 게시글, 사용자 정보, 주문 내역처럼 새로고침해도 남아야 하는 데이터가 여기에 들어갑니다. DBMS(Database Management System)는 그 데이터를 넣고 찾고 바꾸기 쉽게 해 주는 프로그램입니다. MySQL, PostgreSQL, MongoDB 같은 이름은 보통 DBMS 종류를 가리킵니다. SELECT name, stock FROM tangerines WHERE stock > 0; SELECT 는 가져올 열을 고릅니다. 여기서는 이름과 재고입니다. FROM 은 데이터를 찾을 표를 지정합니다. WHERE 는 조건을 붙입니다. 재고가 0보다 큰 귤만 찾습니다. 실제 서비스에서는 사용자의 입력을 문자열로 이어 붙여 SQL을 만들지 않는 것이 중요합니다. SQL 인젝션 같은 보안 문제가 생길 수 있으므로, 라이브러리의 파라미터 기능이나 ORM을 사용합니다. 7. 인증과 인가: 둘은 다르다 인증(Authentication) 은 "당신이 누구인가?"를 확인하는 일입니다. 로그인에서 아이디와 비밀번호, 소셜 로그인 등을 확인하는 과정이 여기에 가깝습니다. 인가(Authorization) 는 "그 일을 해도 되는가?"를 확인하는 일입니다. 로그인했다고 해서 다른 사람의 글을 수정할 수 있는 것은 아닙니다. 이 둘을 섞어 생각하면 보안 오류를 찾기 어려웠습니다. 인증은 신원 확인, 인가는 권한 확인으로 나누어 기억하면 편했습니다. 8. 쿠키와 토큰: 로그인 상태를 이어 가는 방법 HTTP는 기본적으로 각각의 요청을 따로 처리합니다. 그래서 서버는 "방금 로그인한 사람"을 저절로 기억하지 않습니다. 쿠키(Cookie)는 브라우저가 저장했다가 같은 사이트 요청에 함께 보낼 수 있는 작은 데이터입니다. 토큰(Token)은 사용자의 권한 정보를 검증할 수 있게 만든 값으로, 어디에 어떻게 보관하고 전송할지는 서비스 구조에 따라 다릅니다. 둘 다 로그인과 관련 있다고 해서 비밀번호를 그대로 저장하면 안 됩니다. HttpOnly , Secure , 만료 시간, HTTPS 같은 설정과 함께 설계해야 합니다. 9. 환경 변수: 코드 밖에 두는 설정값 환경 변수(Environment Variable)는 실행 환경에 따라 달라지는 값을 코드 밖에서 전달하는 방법입니다. 데이터베이스 주소나 비밀 키처럼 저장소에 올리면 안 되는 값에 특히 유용합니다. DATABASE_URL=postgres://example PORT=3000 DATABASE_URL 은 백엔드가 접속할 데이터베이스 위치를 담는 예시입니다. PORT 는 서버 프로그램이 요청을 기다릴 번호를 정합니다. .env 파일은 편리하지만, 비밀 값이 있다면 Git에 올리지 않도록 .gitignore 에 추가해야 합니다. 10. Git과 GitHub: 코드의 타임머신과 공유 공간 Git은 파일 변경 이력을 기록하는 버전 관리 시스템입니다. 실수한 코드를 되돌리거나, 왜 바꿨는지 확인할 때 도움이 됩니다. GitHub는 Git 저장소를 온라인에서 공유하고 협업할 수 있게 해 주는 서비스입니다. Git과 GitHub는 같은 말이 아니라는 점이 처음엔 특히 헷갈렸습니다. git status git add . git commit -m "귤 목록 API 추가" git status 는 지금 바뀐 파일 상태를 보여 줍니다. git add . 는 다음 기록에 포함할 변경을 선택합니다. 처음에는 . 대신 파일을 하나씩 확인해도 좋습니다. git commit 은 선택한 변경을 메시지와 함께 하나의 기록으로 남깁니다. 11. 배포와 CI/CD: 내 컴퓨터 밖에서도 실행하기 배포(Deployment)는 개발한 서비스를 사용자가 접속할 수 있는 환경에 올리는 일입니다. 내 컴퓨터에서는 잘 되는데 배포 환경에서 안 되는 이유는 운영체제, 설정값, 네트워크가 다를 수 있기 때문입니다. CI(Continuous Integration)는 코드가 합쳐질 때 테스트와 검사를 자주 자동 실행하는 흐름입니다. CD(Continuous Delivery 또는 Deployment)는 검증된 결과를 배포 가능한 상태로 만들거나 실제 배포까지 자동화하는 흐름을 말합니다. 팀마다 CD의 마지막 D를 다르게 쓰므로, 문서에서 범위를 확인하는 습관이 필요합니다. 12. Docker: 실행 환경을 상자에 담는 도구 Docker는 애플리케이션과 필요한 실행 환경을 컨테이너라는 단위로 묶어 실행하는 도구입니다. "내 컴퓨터에서는 되는데요?" 문제를 줄이는 데 도움이 될 수 있습니다. 다만 Docker가 모든 배포 문제를 자동으로 해결해 주지는 않습니다. 비밀 값 관리, 데이터 보관, 네트워크 설정, 이미지 크기 같은 일은 여전히 챙겨야 합니다. 초보자가 자주 헷갈릴 부분 API와 서버는 같은 말일까? API는 요청을 받는 규칙과 창구이고, 서버는 그 요청을 처리하는 프로그램 또는 환경입니다. 서버 하나가 여러 API를 제공할 수 있습니다. 데이터베이스를 프론트엔드에서 바로 호출해도 될까? 개발 중 테스트 도구가 직접 연결되는 경우는 있어도, 일반적인 웹서비스에서는 백엔드를 거치게 합니다. 데이터 접근 권한과 검증 로직을 화면에 노출하지 않기 위해서입니다. 백엔드가 있으면 프론트엔드는 필요 없을까? 아닙니다. 백엔드는 데이터를 처리하고, 프론트엔드는 그 결과를 사람이 이해하고 조작할 수 있는 화면으로 만듭니다. 둘은 역할이 다릅니다. 실제로 적용하며 느낀 한계 이 글은 용어 사이의 연결을 잡기 위한 첫 지도입니다. REST, GraphQL, 캐시, 메시지 큐, 로드 밸런서처럼 더 깊이 들어가면 새로운 선택지가 계속 나옵니다. 또한 기술 선택에는 정답이 하나가 아닙니다. 작은 개인 프로젝트와 많은 사용자가 동시에 접속하는 서비스는 필요한 구조와 비용이 다를 수 있습니다. 다음에 개선하고 싶은 점 다음 글에서는 이 용어들을 실제 미니 프로젝트에 붙여 보고 싶습니다. 로그인한 사용자가 귤 목록을 저장하고 다시 불러오는 기능을 만들면서, 요청·응답과 데이터베이스 흐름을 직접 확인해 보려고 합니다. 참고한 공식 문서 MDN: Client-server overview MDN: Overview of HTTP GitHub Docs: About Git Docker Docs: What is Docker? 아직 배워 가는 초보 개발자입니다. 내용 중 잘못 이해한 부분이나 더 좋은 방법이 있다면 한 수 가르쳐 주시면 감사하겠습니다. 😊
1. 빈자리 취소석 자동 매칭 서비스, 샥 "샥(syak)"은 뷰티 샵의 예약 취소로 발생하는 빈자리 취소석을 사용자에게 자동으로 매칭해 주는 서비스다. 사용자는 취소석 알림을 통해 비어 있는 예약 슬롯을 빠르게 확보할 수 있고, 제휴 샵은 노쇼나 갑작스러운 취소로 인한 매출 타격을 방지한다. 이번 주에는 서비스의 전국 단위 확장과 이를 안정적으로 제어하기 위한 내부 백오피스 웹( syak_admin ) 구축, 그리고 백엔드( syak_BE )와 프론트엔드( syak )의 인프라 및 성능 최적화 작업을 수행했다. 2. 전국 확장과 관리자 시스템 도입 단계의 병목 현상 서비스 대상 지역을 전국으로 확장하고 이를 운영할 관리자 도구를 도입하는 과정에서 여러 시스템적 병목과 설정 오류가 발생했다. 첫째, 로컬 개발 환경에서 Docker Compose로 백엔드를 실행할 때 데이터베이스와 Redis 연결 실패( ECONNREFUSED )로 애플리케이션 컨테이너가 즉시 종료되었다. 호스트 컴퓨터 기준의 연결 설정이 컨테이너 내부 네트워크에 그대로 적용된 것이 원인이었다. 둘째, 관리자 페이지에서 전체 샵 목록을 조회할 때, 4만 행이 넘는 데이터에 대해 JSONB 내부의 전화번호 필드( detail->>phone )를 정렬 조건과 함께 조회하자 Postgres의 JSONB detoast 비용으로 인해 statement timeout (57014) 에러가 발생하며 서버가 중단되었다. 셋째, 운영 환경에서 NVIDIA FLUX 모델을 사용한 마케팅 이미지 생성 API가 500 에러를 반환했다. 이미지 생성 프롬프트 레시피가 담긴 JSON 파일이 Docker 빌드 단계에서 운영 컨테이너 내부로 복사되지 않아 발생한 문제였다. 넷째, 운영 환경의 쿠키 설정 오류가 확인되었다. COOKIE_SAME_SITE=none 환경에서 COOKIE_SECURE=false 로 잘못 설정되어 있어, 최신 브라우저 스펙에 의해 클라이언트의 쿠키 저장이 거부될 위험이 있었다. 3. 안정성과 효율성을 고려한 아키텍처 판단 Docker 환경의 통신 문제를 해결하기 위해, 호스트 기준인 localhost 주소 대신 Compose 서비스명( db , redis )을 사용하도록 환경변수를 덮어쓰고 데이터베이스와 Redis가 준비된 후에 애플리케이션이 실행되도록 의존성 조건을 강화했다. JSONB 조회 타임아웃 문제를 해결하기 위해, 한 번의 쿼리로 대량의 JSONB 필드 정렬과 상세 조회를 동시에 처리하는 구조를 폐기했다. 대신 1단계에서 인덱스를 타는 기본 필드로 목록을 페이징 조회한 후, 해당 페이지에 노출할 샵 ID들만 대상으로 2단계 상세 조회를 수행하도록 쿼리를 분리했다. 이미지 생성 API의 안정성을 높이기 위해 Dockerfile 빌드 단계에 레시피 JSON 파일을 명시적으로 포함했다. 외부 설정 누락이나 파일 누락 등의 오류는 서버 자체의 결함이 아니므로 ImageGenConfigError 라는 커스텀 에러를 정의해 503(Service Unavailable) 응답으로 명확히 구분하여 반환하도록 설계했다. 쿠키 거부 문제를 예방하기 위해 운영 환경의 쿠키 설정을 COOKIE_SECURE=true 로 정정하여 HTTPS 환경에서 안전하게 전달되도록 조치했다. 관리자 웹은 백엔드와 동일 출처 구조로 Nginx가 프록시하도록 설계하여 CORS 설정 없이 HttpOnly , SameSite=Strict 쿠키 세션을 온전히 활용할 수 있게 했다. 4. 변경 전과 변경 후 4-1. Docker Compose 환경변수 및 의존성 수정 ( syak_BE ) 기존에는 컨테이너 내부 통신 주소가 호스트 기준으로 고정되어 연결 실패가 일어났다. 이를 서비스명을 통한 내부 라우팅 방식으로 변경하고 헬스체크 대기 옵션을 적용했다. # docker-compose.yml services: app: build: . ports: - "3000:3000" env_file: - .env environment: DATABASE_URL: postgresql://syak:syak_dev_password@db:5432/syak_dev SUPABASE_DATABASE_URL: postgresql://syak:syak_dev_password@db:5432/syak_dev REDIS_URL: redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_healthy restart: unless-stopped 4-2. 이미지 생성 API 안정화 및 에러 분리 ( syak_BE ) 도커 이미지 빌드 시 필요한 설정 파일을 포함하고, 설정 누락 예외를 500 에러가 아닌 503 에러로 안전하게 분리했다. // src/contexts/admin/infrastructure/MarketingImageService.ts export class ImageGenConfigError extends Error {} let cached: Recipes | null = null; function loadRecipes(): Recipes { if (!cached) { const p = resolve(process.cwd(), 'scripts/marketing/image-recipes.json'); try { cached = JSON.parse(readFileSync(p, 'utf8')) as Recipes; } catch { throw new ImageGenConfigError(`이미지 레시피 파일을 찾을 수 없습니다 (${p}). Dockerfile의 COPY 확인 필요`); } } return cached; } // src/contexts/admin/interface/AdminController.ts try { const result = await runImageGeneration(this.sbClient, count); res.json(result); } catch (err) { if (err instanceof ImageGenConfigError) { console.error('[generateMarketingImages] config', err.message); res.status(503).json({ code: 'IMAGE_GEN_UNAVAILABLE', message: err.message }); return; } next(err); } 4-3. 취소석 알림 버튼 그라데이션 애니메이션 효과 추가 ( syak ) 사용자의 알림 신청 유입률을 높이기 위해 지도 우하단 버튼에 움직이는 그라데이션 테두리 효과를 도입하고, 미지원 브라우저용 폴백을 마련했다. /* src/index.css */ @property --shak-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; } @keyframes shak-spin { to { --shak-angle: 360deg; } } .shak-glow::before { content: ""; position: absolute; inset: -3px; border-radius: inherit; padding: 3px; background: conic-gradient( from var(--shak-angle), #ec4899, #f59e0b, #fde047, #ec4899, #8b5cf6, #ec4899 ); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; animation: shak-spin 2.6s linear infinite; z-index: -1; } @supports not (background: conic-gradient(from var(--shak-angle), red, blue)) { .shak-glow::before { background: linear-gradient(135deg, #ec4899, #f59e0b, #8b5cf6); animation: none; } } 5. 결과 인프라 최적화와 백오피스 체계 구축을 통해 서비스 운영의 안정성과 확장성이 크게 강화되었다. 첫째, 경북 구미시 703곳의 데이터 수집을 시작으로 대전·울산·세종·강원·충청·제주 및 남부 지방 전역을 포함하는 전국 64,221곳의 샵 데이터 수집과 5,767곳의 예약 연동 백필을 무인 파이프라인으로 완주했다. 이와 함께 SEO 대상 지역이 147개 지역으로 대폭 확장되었다. 둘째, 새로 구축된 관리자 페이지를 통해 운영진은 대용량 샵 데이터를 카테고리 및 지역 필터로 타임아웃 없이 안전하게 조회할 수 있게 되었다. SSE 실시간 스트림을 연동하여 검토 대기 중인 도입 문의가 발생하면 실시간으로 상단 종 알림 배지가 활성화되는 모니터링 환경을 갖췄다. 셋째, 매일 08:20 KST에 실행되는 GitHub Actions 자동화 프로세스가 인스타그램, 쓰레드, 메타 광고 지표를 수집하고 Gemini AI의 조언을 덧붙여 Supabase에 성공적으로 기록하기 시작했다. 관리자는 수집된 지표를 바탕으로 NVIDIA FLUX 기반의 고품질 피드 이미지를 관리자 화면에서 직접 생성하고 제어할 수 있게 되었다.
[Cogito 개발기 #03] 전투 로직을 GAS로 옮기는 과정 Cogito의 CombatComponent 에는 이전 전투 구현이 주석으로 남아 있다. 그 안에는 공격 종류 판단, 콤보 입력, 몽타주 재생, 차지 공격, 회전 공격, 피해 효과 적용 코드가 들어 있다. 현재 실행되는 코드에서는 이 중 상당 부분이 Gameplay Ability로 이동했다. 이번 글에서는 남아 있는 이전 코드와 현재 코드를 비교해, 전투 기능의 책임이 어떻게 달라졌는지 정리한다. 1. 이전 전투 코드가 처리하던 일 기존 콤보 관련 코드는 CombatComponent 안에서 다음과 같은 흐름을 가졌다. Attack1 / Attack2 ↓ 현재 공격 종류와 실행 조건 검사 ↓ ProcessComboCommand ↓ ComboActionBegin ↓ Montage 재생 ↓ SetComboCheckTimer ↓ ComboCheck ↓ 다음 Section 이동 또는 공격 종료 이 구조에서는 컴포넌트가 충돌 검사뿐 아니라 공격의 시작, 진행, 종료도 관리했다. 차지 공격과 회전 공격까지 들어오면 컴포넌트 안에서 관리할 상태가 늘어난다. 현재 공격 종류, 차지 여부, 몽타주 종료 시 복구할 이동 상태 등을 함께 추적해야 하기 때문이다. 현재 구조에서는 이런 행동 단위의 처리가 Ability로 이동해 있다. 2. 전환 이후 역할 비교 처리 내용 이전 코드 현재 코드 콤보 단계와 선입력 CombatComponent CogitoGA_AttackBase 공격 몽타주 재생과 종료 CombatComponent 공격 Ability와 Ability Task 차지 공격 컴포넌트 내부 함수 CogitoGA_ChargeAttack 회전 공격 컴포넌트 내부 함수 CogitoGA_Tornado 공격 충돌 검사 CombatComponent CombatComponent 기본 공격 경로의 피해 효과 적용 컴포넌트의 이전 코드 CogitoGA_AttackBase::OnHitEventReceived 핵심은 충돌 판정을 유지하면서 행동의 생명주기를 Ability로 옮겼다는 점이다. CombatComponent 는 공격이 누구에게 닿았는지 검사하고, Ability는 해당 공격을 어떻게 진행하고 종료할지 관리한다. 3. 이 구조에서 사용하는 GAS 요소 Cogito의 전투 흐름을 이해하는 데 필요한 요소를 간단히 정리하면 다음과 같다. 요소 이 글에서의 역할 AbilitySystemComponent 캐릭터에게 부여된 Ability와 효과를 관리하는 중심 객체 Gameplay Ability 공격·방어·회피처럼 실행과 종료가 있는 행동 AttributeSet 체력, 공격력, 방어력, 스태미나 같은 수치 Gameplay Effect 비용, 피해, 회복 지연 등의 효과 Ability Task 몽타주 재생이나 이벤트 수신처럼 진행 중인 Ability의 작업 GAS에서는 Ability를 부여하는 일과 실행하는 일이 구분된다. 실행 시에는 조건을 검사하고, CommitAbility 를 통해 설정된 비용과 쿨다운을 처리할 수 있다. 공식 문서 4. 캐릭터에 Ability 부여하기 플레이어의 PossessedBy 에서는 DefaultAbilities 에 설정된 클래스를 순회하며 Ability를 부여한다. if (HasAuthority()) { for (TSubclassOf<UGameplayAbility> AbilityClass : DefaultAbilities) { if (AbilityClass) { AbilitySystemComponent->GiveAbility( FGameplayAbilitySpec( AbilityClass, 1, INDEX_NONE, this ) ); } } } 이후 ASC의 Actor Info를 초기화한다. AbilitySystemComponent->InitAbilityActorInfo( this, this ); 현재 플레이어 코드에서는 Owner와 Avatar에 모두 플레이어 캐릭터 자신을 전달한다. GiveAbility 는 실행할 수 있는 Ability를 등록하는 단계다. 실제 공격은 이후 입력 처리에서 별도로 요청한다. 5. 입력은 실행 요청으로 바뀐다 일반 약공격을 처음 실행하는 경로에서는 태그로 Ability 활성화를 요청한다. FGameplayTag AttackTag = FGameplayTag::RequestGameplayTag( TEXT("Ability.Attack.Light") ); AbilitySystemComponent->TryActivateAbilitiesByTag( FGameplayTagContainer(AttackTag) ); 이미 공격 중이면 같은 공격을 새로 시작하지 않고 콤보 입력 이벤트를 전달한다. FGameplayEventData Payload; AbilitySystemComponent->HandleGameplayEvent( FGameplayTag::RequestGameplayTag( TEXT("Event.Attack.ComboInput") ), &Payload ); 이렇게 입력 처리와 콤보 진행을 나눈다. 캐릭터는 새 공격을 요청할지, 진행 중인 공격에 입력을 전달할지 판단한다. 다음 콤보로 넘어가는 시점과 단계 관리는 Ability가 담당한다. 6. Ability 안에서 실행 흐름 관리하기 CogitoGA_AttackBase 는 활성화되면 먼저 비용과 쿨다운 커밋을 시도한다. if (!CommitAbility( Handle, ActorInfo, ActivationInfo)) { EndAbility( Handle, ActorInfo, ActivationInfo, true, true ); return; } 그다음 회복 지연 효과를 적용하고, 공격 몽타주를 확인한 뒤 실행 상태를 초기화한다. CurrentCombo = 1; bHasNextComboCommand = false; 몽타주 재생에는 UAbilityTask_PlayMontageAndWait 를 사용한다. UAbilityTask_PlayMontageAndWait* PlayMontageTask = UAbilityTask_PlayMontageAndWait:: CreatePlayMontageAndWaitProxy( this, NAME_None, AttackMontage ); 현재 코드는 완료와 블렌드 아웃을 정상 종료 함수로 연결하고, 중단과 취소를 취소 종료 함수로 연결한다. 공격 실행 중에는 두 가지 이벤트도 기다린다. Event.Attack.Hit : 타격 결과 전달 Event.Attack.ComboInput : 다음 콤보 입력 전달 콤보 입력 대기는 ComboActionData 가 있을 때만 설정한다. 따라서 콤보 데이터가 없어도 몽타주가 있으면 단발 공격 경로로 실행할 수 있다. 7. 적중 결과를 Ability에 전달하기 전투 컴포넌트는 충돌 결과를 다음과 같이 전달한다. FGameplayEventData Payload; Payload.Instigator = Character; Payload.Target = HitActor; Payload.EventMagnitude = DamageMultiplier; UAbilitySystemBlueprintLibrary::SendGameplayEventToActor( Character, GetAttackHitEventTag(), Payload ); 여기서 이벤트를 보내는 대상은 공격자 캐릭터다. 맞은 적은 Payload.Target 에 들어 있다. 공격자의 Ability가 이 이벤트를 받아 대상의 ASC를 찾고 피해 효과를 적용한다. 이 구조에서는 역할이 다음처럼 나뉜다. CombatComponent └─ 적중 대상과 공격 배율 전달 Attack Ability └─ 공격자·대상의 능력치 조회 피해량 계산 Gameplay Effect 적용 8. 종료 처리도 Ability 안으로 옮긴다 공격이 끝나면 콤보 타이머와 내부 상태를 정리한다. if (UWorld* World = GetWorld()) { World->GetTimerManager().ClearTimer( ComboTimerHandle ); } CurrentCombo = 0; bHasNextComboCommand = false; Super::EndAbility( Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled ); 공격 시작 함수만 옮기고 종료 처리를 남겨 두면 책임 분리가 불완전해진다. 현재 코드는 콤보 상태와 타이머를 Ability에서 생성하고 같은 Ability의 종료 시점에 정리한다. 이 연결이 전환 과정에서 중요한 부분이다. 다음 글에서는 이 연결에 사용되는 Gameplay Tag를 다룬다. 행동을 식별하는 태그, 현재 상태를 나타내는 태그, 이벤트를 구분하는 태그를 나누어 살펴본다. 관련 코드 CombatComponent.cpp CogitoGA_AttackBase.cpp MyCharacterPlayer.cpp
아프리카의 가난에 대한 이야기를 들을 때 늘 비슷한 이미지가 떠오른다. 깨끗한 물이 부족한 마을, 오랜 시간을 걸어서 물을 길어오는 아이들, 제대로 된 의료시설이 없는 지역, 낡은 집과 부족해 보이는 생활환경 ... 우리는 이런 모습을 보면서 자연스럽게 그들을 가난하다고 여기고 그들을 도와야 한다고 생각한다. 그런데 어느 순간 이런 생각이 들었다. 우리는 무엇을 기준으로 그들을 가난하다고 판단하고 있는 걸까. 우리가 수도, 전기, 병원, 학교가 있고, 자동차와 인터넷이 있다. 필요한 것이 있으면 돈을 주고 구입하고, 문제가 생기면 문제를 해결하기 위한 서비스를 이용한다. 이런 환경에서 살아온 사람이 그와는 다른 방식으로 살아가는 공동체를 바라볼 때 그들의 생활이 불편하고 부족하게 보일 수도 있다. 하지만 그것이 곧 그들의 삶의 질이 낮다는 뜻일까? 그들은 우리가 그곳에 가기 훨씬 전부터 자신들에게 주어진 환경 안에서 살아왔다. 물을 구하기 위해 먼 거리를 걷는 것도, 자연환경에 맞춰 집을 짓는 것도, 가족과 공동체를 중심으로 살아가는 것도 오랜 시간 그들의 삶을 구성해온 방식이었을 것이다. 물론 그 과정에는 굶주림도 있었을 수 있고, 질병 때문에 치료받지 못한 채 죽는 사람도 있었을 것이다. 그렇다고 해서 그들의 인생 전체를 안타까운 삶이나 실패한 삶이라고 규정할 수 있을까. 한 사람의 삶은 그 사람이 가진 물건의 개수나 우리가 생각하는 편리함의 정도만으로 설명되지 않는다. 어쩌면 우리는 우리가 만들어온 삶의 방식을 '발전'이라고 이름 붙이고, 그 기준에서 멀리 떨어져 있는 사람들을 '가난하다'고 규정해온 것은 아닐까 하는 생각이 들었다. 그렇다고 전통적인 삶을 그대로 두는 것이 무조건 옳다고 생각하는 것은 아니다. 깨끗한 물을 원하는 사람이 있다면 깨끗한 물을 얻을 수 있어야 하고, 치료받기를 원하는 사람이 있다면 의료서비스에 접근할 수 있어야 한다. 교육받고 싶은 사람에게 교육의 기회가 있어야 하고, 자신의 마을을 떠나 도시에서 새로운 삶을 살고 싶은 사람에게도 그 선택이 열려 있어야 한다. 문제는 그 선택이 어디에서 시작되는가라고 생각한다. 외부에서 온 사람이 한 공동체를 바라보고 먼저 문제를 정의한다면 “당신들은 물을 얻기 위해 너무 오래 걷고 있습니다.” “당신들의 집은 너무 열악합니다.” “당신들의 생활방식은 비효율적입니다.” 그리고 우리가 생각하는 해결책을 그들에게 주입한다면 "우물을 만들어 드리겠습니다." "신식 건물을 지어드리겠습니다." "제대로 된 제도가 없군요. 새로운 제도를 만들어 드리겠습니다." 좋은 의도일 수 있다. 실제로 사람들의 삶을 크게 개선하기도 할 것이다. 하지만 좋은 의도가 있다는 사실만으로 그 과정이 항상 옳다고 말할 수 있을까. 나는 점점 '선택권을 준다'는 표현조차 조금 이상하게 느껴졌다. 우리가 그들에게 선택권을 '주는' 사람이 되어서는 안 되는 것 아닐까. 그들의 삶에서 무엇이 필요한지를 발견하고, 무엇을 바꾸고 싶은지를 결정하는 과정은 가능하면 그들 내부에서 시작되어야 한다 . 어느 날 공동체 사람들이 자신들의 경험 속에서 이렇게 생각할 수 있다. '물을 구하러 가는 데 너무 많은 시간이 드니까 조금 더 가까운 곳에서 깨끗한 물을 구할 방법이 있으면 좋겠다'라고. 그때는 외부의 기술과 자원이 필요할 수 있다. 중요한 것은 문제의 시작점과 결정권이 어디에 있느냐 는 것이다. 흔히 원조에 대해 “물고기를 주는 것보다 물고기 잡는 법을 가르쳐야 한다”는 말을 한다. 우물의 예시도 마찬가지일 것이다. 우물을 하나 뚫어주고 떠나는 것보다 사람들이 스스로 우물을 만들고 관리할 수 있도록 돕는 것이 더 지속가능해 보인다. 하지만 이것도 다시 생각해보면 충분하지 않다. 외부인이 찾아와서 “너희에게 우물이 필요하니 우물을 만드는 방법을 가르쳐주겠다”고 한다면 여전히 무엇이 필요한지를 결정한 사람은 외부인이기 때문이다. 그래서 나는 '주는 것'과 '가르치는 것' 이상을 생각해야 한다고 느꼈다. 그들이 무엇을 하려고 하는지를 먼저 듣고, 그것을 스스로 할 수 있는 것은 무엇인지 알고, 필요한 부분, 부족한 부분을 돕는 것이다. 우물을 원하는 공동체에는 우물을 만들 수 있는 기술과 장비를 연결해주고, 학교를 필요로 하는 공동체에는 단순히 건물을 지어주는 것을 넘어 교사를 키우고 운영할 수 있는 구조를 함께 만드는 것이다. 병원이 필요하다면 병원 건물 하나를 세우는 것으로 끝나는 것이 아니라 의료인을 양성하고 의약품을 공급하고 시설을 지속적으로 운영할 방법까지 지역 안에 남아야 한다. 좋은 도움의 결과는 결국 외부의 도움이 계속 필요한 상태가 아니라, 외부의 도움이 점점 필요 없어지는 상태여야 하지 않을까. 물론 모든 상황을 이렇게 이상적으로 바라볼 수는 없다. 전쟁이나 자연재해가 일어났거나 지금 당장 깨끗한 물이 없어 사람들이 죽어가고 있다면 먼저 물을 공급해야 한다. 감염병이 퍼지고 있는데 지역이 스스로 의료체계를 만들 때까지 기다릴 수는 없다. 사람의 생명이 걸린 상황에서는 외부의 즉각적인 지원이 필요하다. 하지만 긴급한 구조와 한 사회의 장기적인 발전은 다른 문제라고 생각한다. 사람을 살리는 것과 그 사람의 삶을 대신 설계하는 것은 다르다. 결국 내가 생각하게 된 것은 '개발을 할 것이냐 말 것이냐'의 문제가 아니었다. 누가 문제를 정의하는가. 누가 더 나은 삶이 무엇인지 결정하는가. 누가 변화의 속도와 방향을 결정하는가. 우리는 다른 사람에게 더 나은 삶을 만들어줄 수 있다고 쉽게 생각한다. 하지만 우리가 생각하는 '더 나은 삶'이 그 사람에게도 같은 의미인지는 알 수 없다. 현대적인 집에서 사는 것이 반드시 더 좋은 삶이라는 보장도 없고, 더 많은 돈을 버는 것이 모든 사람에게 가장 중요한 목표라는 보장도 없다. 반대로 전통적인 삶이라고 해서 반드시 아름답게 보존해야 하는 것도 아니다. 그 안에서 살아가는 사람이 변화를 원한다면 그 변화 역시 존중받아야 한다. 그래서 나는 좋은 개발의 모습을 이렇게 생각하게 됐다. 외부인이 한 공동체를 바라보며 “당신들은 이렇게 살아야 합니다”라고 말하는 것이 아니라, 그곳에서 살아가는 사람들이 자신들의 삶을 바라보며 “우리는 이렇게 살아보고 싶습니다”라고 말할 수 있는 것. 그리고 외부인은 그들이 원하는 방향으로 나아가는 과정에서 부족한 기술이나 자원, 지식과 연결을 제공하는 것. 결국 도움의 목적은 그들을 우리처럼 만드는 것이 아니라, 그들이 자신들의 삶을 스스로 만들어갈 수 있게 하는 것이 아닐까. 처음에는 단순한 '우리는 정말 아프리카 사람들을 가난하다고 말할 수 있을까'라는 생각이었다. 그런데 생각을 이어가다 보니 질문이 조금 달라졌다. '누군가의 삶을 바라보며 가난하다거나 발전하지 못했다고 말하기 전에, 나는 먼저 그 사람의 기준으로 그 삶을 바라본 적이 있는가.' 그리고 누군가를 돕겠다고 나설 때, 내가 그 사람에게 필요한 것을 알고 있다고 너무 쉽게 생각하고 있는 것은 아닌가. 어쩌면 진짜 도움은 무엇인가를 가져다주는 것에서 시작하는 것이 아니라, 먼저 그들의 이야기를 듣는 것에서 시작해야 하는지도 모르겠다.
지난달에 같은 주제로 글을 하나 썼습니다. "죽은 세션 이어서 해줘"라고 말했다가 멀쩡히 살아있던 코디네이터와 이중 운전이 날 뻔한 이야기였어요. 그때는 사고 직전에 알아채고 물러났습니다. 이번엔 못 알아챘습니다. 그래서 워커 브랜치에 커밋이 하나 더 생겼어요. 먼저 결론부터 봅시다. tmux ls 는 워커의 생존을 증명하지, 코디네이터의 부재를 증명하지 않습니다. 재개 프로토콜은 보통 "디스크에 남은 상태를 측정하라"고 말하는데, 정작 가장 위험한 상태 — 지금도 살아서 같은 런을 몰고 있는 다른 코디네이터 — 는 디스크에 없습니다. 창이 꺼졌으니 죽은 거라고 생각했다 시작은 평범한 요청이었습니다. 이전 진행하던 세션이 꺼졌는데, 다시 resume 해줄 수 있어? 디자인 개편 관련 세션이야 상태 파일을 읽었습니다. 태스크 4개짜리 디자인 개편 런이 돌던 흔적이 있었고, 브랜치도 워크트리도 그대로 있었어요. tmux ls 를 쳤더니 워커 세션 4개가 살아 있었습니다. 그런데 코디네이터 창은 없었습니다. 워커만 넷, 코디네이터는 없음. 저는 이걸 "코디네이터가 죽었다"의 증거로 읽었어요. 지금 보면 이게 첫 번째 실수입니다. 저는 없음을 확인한 게 아니라, 제가 본 목록에 없다는 것만 확인 했거든요. 그래서 코디네이터 역할을 이어받았습니다. 상태 파일을 읽고, 리뷰 대기 중이던 워커에게 커밋 승인 프롬프트를 보냈습니다. 이상 신호는 파일 시간에서 나왔다 리뷰 산출물을 확인하다가 뭔가 걸렸습니다. ls -lt .orchestration/reviews/ 리뷰 파일 두 개의 mtime이 제 세션 시작 시각보다 나중 이었어요. 제가 만들지 않은 파일이 제가 시작한 뒤에 생겨 있었습니다. 여기서 "이상하네" 하고 넘어갈 수도 있었는데, 다행히 프로세스 테이블을 뒤졌습니다. ps -Ao pid,ppid,lstart,command | grep -i "[c]laude" | grep -v grep 살아 있었습니다. PID 54780, 13:04:48 시작 — 세 시간째 같은 런을 몰고 있었습니다. cwd까지 확인했어요. lsof -a -p 54780 -d cwd 왜 tmux ls 에 안 잡혔는지도 그때 알았습니다. 이 코디네이터는 tmux 창이 아니라 ssh pty 위에서 도는 순수 claude 프로세스 였거든요. 붙을 tmux 창 자체가 없었습니다. 감시 프로세스도 따로 살아 있었어요 — watch-status.sh --tasks ... done 3 이 15:57부터 워커 완료를 기다리는 중이었습니다. 내가 만든 충돌 문제는 그 사이에 제가 이미 개입했다는 겁니다. 제가 보낸 "커밋 승인, 코드 변경 금지" 프롬프트가, 진짜 코디네이터가 보낸 리워크 지시와 같은 워커에 동시에 꽂혔습니다. 워커는 리워크 수정을 마치고 커밋했는데, 결과적으로 그 브랜치에는 커밋이 두 개( 9177841 → d3a378b ) 생겼어요. 이 런의 완료 조건은 태스크당 단일 커밋이었습니다. 다행히 코드 내용 자체는 리뷰 요구대로 들어가 있었고 워크트리도 깨끗했습니다. 데이터가 깨진 건 아니에요. 하지만 이건 운이 좋았던 것에 가깝습니다. 분산 시스템에서 말하는 split-brain이 그대로 재현된 상황이었고, 여기서 제가 머지까지 밀어붙였다면 이야기가 달라졌을 겁니다. 확인한 뒤로는 아무것도 하지 않았습니다. 이중 커밋도 진짜 코디네이터의 판단에 맡기고 그대로 뒀어요. 대신 살아있는 코디네이터의 트랜스크립트를 읽어서 화면에 미러링만 해줬습니다. 그래서 뭘 먼저 확인해야 하나 지난번 글을 쓰고도 같은 사고를 낸 이유는 명확합니다. 저는 "코디네이터가 살아있을 수 있다"는 개념 은 알고 있었지만, 그걸 확인하는 절차 를 가지고 있지 않았어요. 그래서 이번엔 절차로 적어둡니다. 역할을 인수하기 전에 두 줄: # 1. 감시 프로세스가 살아있는가 (tmux 밖도 잡힌다) ps -Ao lstart,command | grep watch-status.sh # 2. 산출물이 내 세션 시작 이후에 움직였는가 ls -lt .orchestration/reviews/ 첫 줄은 프로세스 테이블을, 둘째 줄은 파일 mtime을 봅니다. 둘 다 조용해야 비로소 "부재"입니다. 하나라도 움직이면 인수하면 안 됩니다. 이 두 줄이 값싸다는 게 핵심이에요. 합쳐서 1초도 안 걸리는데, 반나절짜리 충돌을 막습니다. 남은 것 "세션이 죽었다"는 건 사람의 인지이지 검증된 사실이 아닙니다. 지난 글에서 이미 썼던 문장인데, 이번에 다시 확인했어요. 개념을 아는 것과 절차를 갖는 것은 다릅니다. 그리고 하나 더. 창이 안 보인다고 프로세스가 없는 게 아닙니다. tmux, 원격 pty, 백그라운드 — 에이전트를 여러 갈래로 띄우기 시작하면 "내가 보는 목록"과 "실제로 도는 것"은 점점 벌어집니다. 목록이 아니라 프로세스 테이블을 믿으세요.
AI 코딩 에이전트에 이미지·음악 생성 AI까지 붙이면, 이제 모바일 게임을 혼자 만들 수 있습니다. 그럼 실제로 돈은 얼마나 들고, 며칠이나 걸릴까요? 장르별로 계산해 본 결과를 정리했습니다. 숫자는 직접 만든 AI 게임 제작 비용 계산기 로 뽑았고, 가격은 2026년 10월 5일 기준입니다. 규모 기준은 AI 에이전트로 실제 1인 개발한 모바일 게임입니다(소형 약 2주). 결론 소형 모바일 게임이면 2–3주, 약 $50–180 비용의 약 66%는 코딩 AI 구독료 이미지 86장 정도는 Midjourney 최저 요금제(월 $10)로 충분 장르별 (소형) 장르 기간 이미지 가성비 조합 보통 조합 하이퍼캐주얼 6–10일 47장 $46–65 $128–181 비주얼노벨 12–20일 87장 $53–75 $128–181 머지 14–22일 86장 $53–75 $128–181 카드·덱빌딩 16–26일 109장 $57–80 $128–181 가성비 : Gemini(Nano Banana 2) + Pixabay 무료 음원 + Google AI Pro(Gemini CLI) 보통 : Midjourney + Suno + ElevenLabs + Claude Max 5× 둘 다 Google Play 등록비 $25가 포함돼 있습니다. 가성비 조합은 보통 조합의 약 40% 비용입니다. 비용 내역 (머지·소형·보통 조합) 항목 비용 비중 코딩 (Claude Max 5×, 1개월) $100 약 66% 스토어 등록비 (Google Play) $25 약 17% 그림 (Midjourney) $10 약 7% 음악 (Suno) $10 약 7% 효과음 (ElevenLabs) $6 약 4% 계산 기준 이미지 수 = 캐릭터 × 애니메이션 프레임 + 배경 + 아이템 + UI. 3번 뽑아 1장 쓰는 것으로 가정 코딩 : 에이전트가 개발 기간의 약 70% 동안 돌고, 하루 약 2,000만 토큰을 읽는다고 가정(대부분 캐시). 소형 머지 기준 약 2.5억 토큰, Claude Sonnet 5.5 API로 하면 약 $163 구독은 월 단위 라서 개발이 한 달을 넘기면 한 달 치가 더 붙습니다. 기간을 짧게 끊는 게 가장 큰 절약입니다 에이전트에 넣는 첫 프롬프트 계산기는 장르·규모·엔진을 고르면 바로 쓸 수 있는 개발 시작 프롬프트도 만들어 줍니다. 예시(머지·Unity): 당신은 숙련된 게임 개발자입니다. Unity (C#)로 안드로이드용 2D 머지 게임 "내 게임"을 함께 만들어 주세요. 규칙: 코드는 단순하고 모듈화. 한 단계에 기능 하나. 단계가 끝날 때마다 휴대폰에서 테스트하는 방법을 알려 주세요. 진짜 그림을 주기 전까지는 임시 도형을 쓰세요. 1. 프로젝트 설정, 폴더 구조, 씬 흐름(타이틀 → 게임 → 결과) 2. 임시 그림으로 핵심 게임 루프 3. 점수, 성장, 난이도 곡선 ... 그림·배경음악·효과음 프롬프트도 영어로 함께 나와서 Midjourney나 Suno에 그대로 붙여 넣으면 됩니다. 주의할 점 Steam은 AI 생성 콘텐츠를 쓰면 신고해야 합니다. 스토어마다 규칙을 출시 전에 확인하세요 온라인 대전을 넣으면 Photon, Firebase 같은 서버비가 따로 듭니다 출시 후 운영비와 마케팅비는 계산에 넣지 않았습니다 규모별 비용과 게임용 AI 도구 목록까지 정리한 글은 여기 있습니다: AI로 게임 만들기 비용 얼마? 2026년 2D 게임 제작비 정리
머신러닝 회귀 코드순서 라이브러리 불러오기 데이터 불러오기 데이터 전처리 입력값(X)과 정답값(y) 분리 학습용·테스트용 데이터 분리 모델 생성 및 학습 예측 성능 평가 결과 시각화 및 변수 중요도 확인 회귀 : 숫자예측 X : 정보 y : 맞일 데이터 X_train, X_test : 모델 학습용 데이터 y_train, y_test : 모델 성능 확인 데이터 [1, 라이브러리 작성] import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import root_mean_squared_error 데이터 불러오기 base_path = r"데이터 위치" file_name = r"데이터 이름.csv" file_path = os.path.join(base_path, file_name) df = pd.read_csv(file_path) df.head() 3. 전처리 작성 df.info() df.describe() - 불필요한 정보 제거 (이름, 이상치 ...) - 필요없는 열 제거 ✨[자주사용] df.drop(columns="열이름") # 특정 열삭제 df[df["열이름"] < 기준값] # 조건에 맞는 열만 남김 df["새로운 열"] = 계산식 # 새로운 열 만들기 4. 범주형 데이터 변환 - 분류형 -> 수치형 df = pd.get_dummies({참고데이터, columns=[바꿀 열이름], drop_tirst=True, dtype=int}) 5. X와 y분리 작성 X = df.drop(columns=예측대상의 열이름) y = df[예측 대상 열이름] 6. 학습용, 테스트용 데이터 분리 X_train, y_train, y_test, y_train = train_test_split( X, y, test_size=0.2, random_state=2026 ) 7. 결정 트리 - 작성법 (예시) tree_model = DecisionTreeRagressor( max_depth=6, # 최대 깊이 min_samples_leaf=5, # 최소 데이터 수 random_state=2026 ) tree_model.fit(X_train, y_train) ⭐️ .fit :모델 학습 (예측과 평가) tree_pred = tree_model.predict(X_test) ⭐️ .predict : 예측 tree_rmse = root_mean_squared_error(y_test, tree_pred) 실제 가격과 모델이 예측한 가격차이가 얼마나 차이나는지 계산(확인) 실제 정답 모델의 예측 ↓ ↓ y_test tree_pred └─────────┬─────────────┘ ↓ root_mean_squared_error() ↓ 오차 계산 ↓ RMSE 8. 랜덤 포래스트 작성법 - 여러개의 결정 트리를 만든 뒤 각 트리의 예측을 평균 내는 모델 (예시) rf_model = RandomForestRegressor( n_estimators=200, # 결정트리 200생성 random_state=2026 # 실행결과 고정 ) 2. 학습 rf_model✨.fit(X_train, y_test) # rf_model: 학습이 끝난 랜덤포레스트 3. 예측 rf_pred = rf_model✨.predict(X_test) # predict: 예측해라, X_test: 4. 평가 rf_rmse = root_mean_squared_error(y_test, rf_pred) # rf_pred:모델이 예측한 문제 # ✨실제값과 예측값 비교-> RMSE 계산 5. 출력 print(f"랜덤 포레스트 RMSE: {rf_rmse:.3f}") # 소수점 3자리 까지 표시 (✨ = 중요표시 ) -⭐️ 회귀 랜덤 포레스트에서는 각 트리가 내놓은 예측값을 평균함 - rf_pred : X_test 정체에 개한 예측값 묶음-> 리스트로 반환됨 (rf_pred = rf_model.predict(X_test) ->[1, 2, 3 ...]) - predict(X): 입력된 샘플 각각에 대한 예측값을 반환 모델의 예측 -> rf_pred 실제정답 -> y_test 입력 데이터 ↓ 결정 트리 1 ─┐ 결정 트리 2 ─┼─ 평균 → 최종 가격 예측 결정 트리 3 ─┘ 9. 실제값과 예측값 시각화 sns.scatterplot(x=y_test, y=rf_pred) plt.plot( [y_test.min(), y_test.max()], [y_test.min(), y_test.max()], color="red" ) plt.xlabel("실제 판매 가격") plt.ylabel("예측 판매 가격") plt.show() 10. 변수 중요도 작성법 ``` # ✨표로 만들기 feature_importance = pd.DataFrame({ "feature": X.columns, # "feature"열에 X.columns 데이터 들어감 (X.columns : ✨변수명) "importance": rf_model.feature_importances_ # ^(랜덤 포레스트가 학습한)변수✨중요도 }).sort_valies("importance", ascending=False) # sort_valies() : 값을 기준으로 ✨정렬하는 함수 -> "importance"값을 기준으로 정렬, ascending=False 내림차순 print(feature_importance) sns.barplot( data=feature_importance, x="importance", y="feature" ) pit.title("변수 중요도") plt.show() feature_importances_ : 랜덤 포레스트가 예측할 때 어떤 변수가 상대적으로 중요하게 사용했는지 보여줌 (값이 클수록 랜덤 포레스트가 예측할 때 그 변수를 많이 활용했다는 의미)
힙을 처음 배우면 부모 노드가 자식 노드보다 크거나 작은 완전 이진 트리라고 설명한다. 우선순위 큐는 우선순위가 가장 높은 데이터를 먼저 꺼내는 자료구조라고 배운다. priorityQueue.enqueue({ videoId: "video-101", priority: 10 }); const nextJob = priorityQueue.dequeue(); priority 가 가장 높은 작업을 먼저 꺼낼 수 있다. 최대 힙에서는 가장 큰 값이 루트에 있고, 최소 힙에서는 가장 작은 값이 루트에 있다는 설명도 입문 단계에서는 힙의 동작을 이해하는 데 유용하다. 하지만 실제 영상 처리 서비스에서는 숫자가 가장 큰 작업을 선택하는 것만으로 충분하지 않다. 유료 사용자의 영상은 일반 사용자보다 먼저 처리해야 할 수 있고, 오랫동안 기다린 작업은 우선순위를 높여야 하며, 장애 복구 작업은 모든 일반 작업보다 먼저 실행해야 할 수도 있다. 여러 작업 서버가 동시에 작업을 가져간다면 같은 작업을 두 번 처리하지 않도록 해야 한다. 우선순위는 누가 결정하는가? 작업의 중요도가 바뀌면 힙에 들어 있는 값도 즉시 수정해야 할까? 높은 우선순위 작업이 계속 들어오면 일반 작업은 언제 처리해야 할까? 우선순위 큐에서 먼저 꺼냈다는 사실만으로 한 작업 서버만 처리한다고 보장할 수 있을까? 실제 서비스에서 힙과 우선순위 큐를 사용한다는 것은 모든 데이터를 순서대로 정리하는 일이 아니라, 계속 변하는 작업 집합에서 지금 처리해야 할 하나를 반복해서 선택하는 비용을 줄이는 것이다. 모든 작업을 정렬해야 다음 작업을 선택할 수 있을까? 영상 변환 서비스에는 다음과 같은 작업이 들어온다고 해보자. type VideoJob = { id: string; videoId: string; tier: "free" | "pro"; urgency: "normal" | "incident"; createdAt: Date; }; 가장 단순한 구현은 새로운 작업이 들어올 때마다 전체 배열을 정렬하는 것이다. const jobs: VideoJob[] = []; function addJob(job: VideoJob) { jobs.push(job); jobs.sort((a, b) => { return getPriority(b) - getPriority(a); }); } function takeNextJob() { return jobs.shift(); } 이 코드는 동작한다. 작업 전체가 우선순위순으로 정렬되므로 첫 번째 항목을 꺼내면 다음 작업을 얻을 수 있다. 그러나 작업이 하나 추가될 때마다 전체 목록을 다시 정렬한다. 작업이 n 개라면 일반적인 정렬 비용은 O(n log n) 이다. 영상 작업이 가끔 들어오고 관리 화면에서 전체 순위를 자주 보여줘야 한다면 이 선택도 충분할 수 있다. 반면 서비스가 실제로 반복하는 동작이 다음과 같다면 전체 정렬은 필요한 것보다 많은 일을 한다. 1. 작업 하나가 들어온다. 2. 지금 가장 중요한 작업 하나를 꺼낸다. 3. 새로운 작업이 다시 들어온다. 4. 다시 가장 중요한 작업 하나를 꺼낸다. 두 번째로 중요한 작업과 열 번째로 중요한 작업 사이의 정확한 순서는 당장 필요하지 않다. 지금 처리할 하나만 정확히 찾으면 된다. 힙은 이 요구에 맞춰 전체 순서를 포기한다. 가장 우선순위가 높은 루트만 보장하고 나머지 데이터는 다음 루트를 효율적으로 찾을 수 있는 정도로만 배치한다. const jobQueue = new PriorityQueue<VideoJob>({ compare: (a, b) => compareJobs(a, b) }); jobQueue.enqueue(job); const nextJob = jobQueue.dequeue(); 힙으로 구현된 우선순위 큐에서는 일반적으로 가장 중요한 작업을 확인하는 데 O(1) , 작업을 추가하거나 제거한 뒤 힙의 조건을 복구하는 데 O(log n) 이 필요하다. 여기서 중요한 차이는 힙이 더 빠른 정렬 방법이라는 것이 아니다. 힙은 전체 정렬 결과를 만들지 않는다. 서비스가 실제로 요구하는 “다음 작업 하나 선택하기”에 필요한 부분만 유지한다. 우선순위는 하나의 숫자가 아니라 비즈니스 규칙이다 영상 작업의 우선순위를 단순히 클라이언트가 보낸 숫자로 결정할 수 있을까? // Bad: 사용자가 원하는 우선순위를 직접 결정한다. await videoQueue.enqueue({ videoId: input.videoId, priority: input.priority }); 외부 입력을 그대로 사용하면 무료 사용자가 매우 큰 우선순위를 보내 자신의 작업을 먼저 처리할 수 있다. 우선순위는 요청 데이터가 아니라 서버의 정책으로 계산해야 한다. function calculatePriority( job: VideoJob, now: Date ) { const incidentScore = job.urgency === "incident" ? 10_000 : 0; const tierScore = job.tier === "pro" ? 1_000 : 0; const waitingMinutes = Math.floor( (now.getTime() - job.createdAt.getTime()) / 60_000 ); return incidentScore + tierScore + waitingMinutes; } 장애와 관련된 작업에는 가장 큰 점수를 주고, 유료 사용자의 작업에는 추가 점수를 주며, 오래 기다린 작업에는 대기 시간만큼 점수를 더한다. 하지만 urgency 와 tier 도 클라이언트가 주장한 값을 그대로 사용해서는 안 된다. 서버는 로그인한 사용자의 구독 상태와 운영자가 등록한 장애 정보를 신뢰할 수 있는 저장소에서 확인해야 한다. async function createVideoJob( userId: string, input: CreateVideoJobInput ) { const user = await userRepository.findById(userId); const video = await videoRepository.findOwnedVideo( userId, input.videoId ); if (!user || !video) { throw new Error("Video not found"); } const job = await videoJobRepository.create({ videoId: video.id, tier: user.subscriptionTier, urgency: "normal", status: "queued", createdAt: new Date() }); await videoQueue.enqueue(job); return job; } 현실의 “중요한 영상 작업”은 코드에서 다음과 같이 변환된다. 입력 사용자 ID와 처리할 영상 ID 상태 검증된 구독 등급, 장애 여부, 생성 시각, 작업 상태 출력 현재 정책에 따라 선택된 다음 처리 작업 우선순위 큐는 이 규칙을 실행하는 도구다. 어떤 사용자를 먼저 처리할지는 자료구조가 결정하지 않는다. 제품 정책과 운영 규칙을 비교 함수와 점수 계산으로 표현해야 한다. 점수가 같을 때도 처리 순서를 정의해야 한다 두 작업의 우선순위 점수가 같다면 어떤 작업을 먼저 처리해야 할까? 비교 함수가 점수만 확인하면 같은 우선순위의 작업 순서는 구현 세부사항에 따라 달라질 수 있다. function compareJobs( a: VideoJob, b: VideoJob ) { const priorityDifference = calculatePriority(a, new Date()) - calculatePriority(b, new Date()); if (priorityDifference !== 0) { return priorityDifference; } return b.createdAt.getTime() - a.createdAt.getTime(); } 점수가 같을 때는 먼저 생성된 작업을 우선하도록 두 번째 기준을 둔다. 생성 시각까지 같을 가능성이 있다면 작업 ID와 같은 안정적인 값을 마지막 기준으로 사용할 수 있다. function compareJobs( a: RankedVideoJob, b: RankedVideoJob ) { if (a.priority !== b.priority) { return a.priority - b.priority; } if (a.createdAt.getTime() !== b.createdAt.getTime()) { return b.createdAt.getTime() - a.createdAt.getTime(); } return b.id.localeCompare(a.id); } 이처럼 비교 규칙은 하나의 숫자보다 구체적이어야 한다. 장애 여부, 구독 등급, 대기 시간과 생성 순서가 어떤 순서로 적용되는지 명확해야 같은 입력에서 일관된 결과를 얻을 수 있다. 오래 기다릴수록 우선순위가 높아진다면 무엇이 달라질까? 앞의 calculatePriority() 는 현재 시각을 사용한다. 작업이 큐에 들어간 뒤 시간이 지나면 대기 점수가 올라간다. 하지만 힙은 내부 데이터의 값이 저절로 변했다는 사실을 알지 못한다. const queuedJob = { ...job, priority: calculatePriority(job, new Date()) }; videoQueue.enqueue(queuedJob); 작업을 넣을 때 계산한 점수만 저장하면 한 시간 뒤에도 같은 점수가 남는다. 반대로 작업을 꺼낼 때마다 모든 항목의 점수를 다시 계산하고 힙을 만들면 최신 결과를 얻을 수 있지만, 반복 비용이 커진다. 이 문제에는 하나의 정답만 있는 것이 아니다. 서비스 요구사항에 따라 다음과 같은 선택을 할 수 있다. 대기 시간 점수를 분 단위가 아니라 넓은 구간으로 계산한다. 일정 시간 이상 기다린 작업을 별도의 우선순위 큐로 이동한다. 주기적으로 일부 작업의 우선순위를 다시 계산한다. 무료 작업과 유료 작업에 별도 큐를 두고 정해진 비율로 번갈아 가져간다. 작업을 꺼낼 때 저장소의 현재 상태를 확인하고 오래된 항목이면 다시 등록한다. 중요한 것은 “우선순위가 동적이다”라는 규칙을 단순한 정적 숫자로 축소하지 않는 것이다. 우선순위가 언제 계산되고 언제 다시 평가되는지까지 정의해야 한다. 높은 우선순위 작업만 계속 처리해도 될까? 유료 사용자의 작업이 계속 들어오면 무료 사용자의 영상은 영원히 처리되지 않을 수 있다. 이를 기아 상태라고 한다. 실행 가능한 작업이 큐에 있지만 다른 작업에 계속 밀려 실행 기회를 얻지 못하는 상황이다. 다음 정책은 유료 작업을 항상 무료 작업보다 먼저 처리한다. function getPriority(job: VideoJob) { return job.tier === "pro" ? 100 : 10; } 유료 작업의 유입량이 작업 서버의 처리량보다 많다면 무료 작업은 큐에서 빠져나올 수 없다. 비즈니스가 유료 사용자에게 더 빠른 처리를 약속했더라도 무료 사용자의 작업을 무기한 방치하겠다는 의미는 아닐 수 있다. 대기 시간을 우선순위에 반영하는 에이징 정책을 적용할 수 있다. function getPriority( job: VideoJob, now: Date ) { const tierScore = job.tier === "pro" ? 100 : 10; const waitingHours = Math.floor( (now.getTime() - job.createdAt.getTime()) / 3_600_000 ); return tierScore + waitingHours * 10; } 무료 작업도 오래 기다리면 점수가 올라가 언젠가는 처리될 수 있다. 또 다른 방법은 큐를 분리하고 작업 서버가 가져가는 비율을 정하는 것이다. async function takeNextJob() { if (processedProJobs < 4) { const proJob = await proQueue.dequeue(); if (proJob) { processedProJobs += 1; return proJob; } } processedProJobs = 0; return ( await freeQueue.dequeue() ) ?? proQueue.dequeue(); } 예를 들어 유료 작업 네 개를 처리한 뒤 무료 작업 하나를 처리하도록 정할 수 있다. 이 방식은 하나의 점수로 모든 정책을 표현하지 않고 처리 비율을 명시한다. 어떤 방식이 더 적합한지는 서비스가 약속한 처리 시간과 작업량에 따라 달라진다. 우선순위는 기술적인 정렬 기준이 아니라 어떤 사용자의 기다림을 얼마나 허용할지 결정하는 제품 정책이다. 큐에서 꺼낸 작업이 실제로 처리 가능한지는 다시 확인해야 한다 영상 작업이 우선순위 큐에서 기다리는 동안 사용자가 영상을 삭제하거나 구독을 취소할 수 있다. 운영자가 작업을 중단시켰을 수도 있다. 큐에 들어 있는 데이터가 생성 당시에는 정확했더라도 실행 시점에는 오래된 상태가 될 수 있다. async function processNextJob() { const queuedJob = videoQueue.dequeue(); if (!queuedJob) { return; } await transcodeVideo(queuedJob.videoId); } 이 코드는 큐의 항목을 현재 상태의 원본으로 사용한다. 삭제된 영상이나 이미 취소된 작업도 처리할 수 있다. 큐에는 작업을 찾기 위한 최소한의 정보와 정렬에 필요한 정보를 넣고, 실행 전 데이터베이스에서 현재 상태를 다시 확인하는 편이 안전하다. type QueuedJob = { jobId: string; priority: number; version: number; }; async function processNextJob() { const queuedJob = videoQueue.dequeue(); if (!queuedJob) { return; } const job = await videoJobRepository.findById( queuedJob.jobId ); if ( !job || job.status !== "queued" || job.version !== queuedJob.version ) { return; } const claimed = await videoJobRepository.claimIfQueued(job.id); if (!claimed) { return; } await transcodeVideo(claimed.videoId); } 데이터베이스의 작업 레코드가 상태의 Source of Truth가 되고, 힙에 저장된 항목은 다음 후보를 빠르게 찾기 위한 계산 데이터가 된다. version 이 다르다면 작업의 우선순위나 상태가 큐에 들어간 뒤 변경되었다는 뜻이다. 오래된 항목은 무시하고 최신 버전의 항목을 따로 처리할 수 있다. claimIfQueued() 는 작업 상태가 queued 일 때만 processing 으로 변경해야 한다. 여러 작업 서버가 비슷한 시점에 같은 작업을 선택하더라도 상태 변경에 성공한 하나의 서버만 실제 처리를 시작한다. 힙은 가장 중요한 후보를 선택할 수 있지만, 동시 실행 제어까지 해결하지는 않는다. 작업 소유권은 데이터베이스의 조건부 업데이트나 트랜잭션처럼 여러 서버가 공유하는 저장소에서 보장해야 한다. 우선순위 큐가 필요하지 않은 경우도 있다 작업이 열 개뿐이고 우선순위가 자주 바뀌며 관리 화면에서 전체 순서를 항상 보여줘야 한다면 배열을 정렬하는 구현이 더 단순할 수 있다. 작업이 데이터베이스에 저장되어 있고 여러 서버가 함께 처리한다면 애플리케이션 메모리의 힙보다 데이터베이스 조회가 더 적합할 수도 있다. SELECT id FROM video_jobs WHERE status = 'queued' ORDER BY priority DESC, created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED; 이 쿼리는 처리 가능한 작업 중 우선순위가 가장 높은 하나를 선택하면서 다른 작업 서버가 잠근 행은 건너뛴다. 작업 수와 조회 빈도에 맞는 인덱스가 필요하지만, 여러 서버가 공유하는 작업 상태를 한곳에서 관리할 수 있다. 반대로 한 프로세스 안에서 수많은 후보가 계속 들어오고 가장 중요한 항목을 반복해서 꺼내야 한다면 메모리 힙이 잘 맞는다. 영구 보관이 필요한 작업은 데이터베이스에 저장하고, 빠른 선택을 위한 인덱스 구조만 메모리에 유지하는 조합도 가능하다. 자료구조를 선택할 때는 O(log n) 만 비교해서는 안 된다. 작업이 어디에 저장되는지, 프로세스가 재시작돼도 남아야 하는지, 여러 서버가 같은 큐를 공유하는지까지 함께 판단해야 한다. 우선순위 작업 처리를 설계하기 전에 확인할 질문 서비스가 전체 작업 순서를 필요로 하는가, 지금 처리할 하나만 필요로 하는가? 우선순위를 결정하는 비즈니스 규칙은 무엇이며 어떤 기준이 먼저 적용되는가? 클라이언트가 보낸 우선순위가 아니라 서버가 검증한 상태로 점수를 계산하는가? 우선순위가 같은 작업의 처리 순서를 안정적으로 결정했는가? 대기 시간이나 상태 변화에 따라 우선순위를 다시 계산해야 하는가? 높은 우선순위 작업이 계속 들어올 때 낮은 우선순위 작업이 영원히 기다리지 않는가? 큐에 저장된 항목과 데이터베이스의 현재 상태 중 무엇이 Source of Truth인가? 취소되거나 변경된 작업을 실행 직전에 다시 검증하는가? 여러 작업 서버가 같은 작업을 동시에 처리하지 않도록 소유권을 획득하는 과정이 있는가? 프로세스가 종료되었을 때 메모리의 우선순위 큐를 복구할 수 있는가? 작업 전체를 자주 조회해야 한다면 정렬된 배열이나 데이터베이스 조회가 더 단순하지 않은가? 대기 작업 수, 우선순위별 대기 시간과 기아 상태를 관찰할 수 있는가? 힙은 전체 순서 대신 다음 선택에 집중한다 힙은 모든 데이터를 완전히 정렬하지 않는다. 가장 우선순위가 높은 항목을 빠르게 확인하고, 작업이 추가되거나 제거될 때 다음 선택에 필요한 조건만 복구한다. 따라서 수많은 작업 중 지금 처리할 하나를 반복해서 선택하는 서비스에 잘 맞는다. 하지만 힙을 도입한다고 우선순위 정책까지 자동으로 만들어지는 것은 아니다. 영상 처리 서비스에서는 장애 여부, 구독 등급, 대기 시간과 생성 순서가 어떤 관계를 가지는지 먼저 정의해야 한다. 높은 우선순위 작업 때문에 다른 작업이 영원히 밀리지 않도록 처리 규칙도 설계해야 한다. 또한 메모리의 우선순위 큐는 다음 후보를 고르는 계산 구조일 뿐이다. 작업의 현재 상태와 실행 소유권은 여러 서버가 공유할 수 있는 저장소에서 관리해야 한다. 실행 직전에는 작업이 여전히 유효한지 확인하고, 하나의 작업 서버만 상태 변경에 성공하도록 해야 한다. 결국 힙과 우선순위 큐를 선택한다는 것은 데이터를 모두 정렬하겠다는 뜻이 아니다. 서비스가 반복해서 내려야 하는 “지금 무엇을 먼저 처리할 것인가”라는 결정을 효율적으로 유지하겠다는 설계 판단이다. 다음 글에서는 트리가 단순히 부모와 자식의 관계를 저장하는 구조를 넘어, 계층을 표현하고 탐색 범위를 단계적으로 줄이는 데 어떻게 사용되는지 살펴본다.
창작 AI를 제품에 넣으려는 개발팀은 무엇을 기준으로 도입을 판단해야 할까? 생성 결과의 품질뿐 아니라 사용자가 수정하고 채택하는 과정, 이를 처리하는 운영 환경, 같은 작업을 지속할 비용까지 평가 범위에 넣는 것이 이 글의 제안이다. 창작자의 작업 방식을 연구에 반영하겠다는 발표에, 별개의 GPU 클러스터 구축 사례와 메모리 가격 변화를 함께 놓으면 개발팀이 검토할 조건이 구체화된다. 이 사례들은 서로 연결된 사업이 아니라 각각 연구 방향, 운영 조건, 비용 가정을 살펴보는 근거다. 연구 협력이 밝힌 목표와 아직 공개하지 않은 항목 Google DeepMind 제품 담당 부사장 Eli Collins는 2026년 6월 22일 A24와의 연구 파트너십을 발표했다 . 협력 계획은 여러 프로젝트에서 연구개발을 진행하며 예술가의 새로운 작업 방식과 기법 개발을 지원하는 데 초점을 둔다. 발표문은 A24와 영화 제작자들이 창작 의도에 맞춰 기술의 발전 방향에 관여하고, 이야기 표현의 가능성을 확장할 수 있다고 설명한다. 완성된 도구에 대한 평가를 받는 데서 나아가, 도구를 설계하는 과정에도 창작자의 판단을 반영하려는 구상으로 읽힌다. 소개된 발표 내용은 협력 목표를 설명하지만, 특정 모델·편집 기능·공개 일정·상용 서비스 형태와 제작 시간·비용의 절감 수치는 제시하지 않는다. 따라서 현재 제품 요구사항으로 가져올 대상은 기능 목록이 아니라 창작자의 요구를 연구개발에 반영하는 접근이다. 수정 요청을 제품의 검토 항목으로 바꾸기 창작자가 결과를 고쳐 쓰는 도구를 설계한다면, 다음 항목을 검토할 만하다. 아래는 협력 취지를 제품 설계에 적용한 검토 항목 예시다. 결과의 일부만 바꾸려 할 때 사용자가 수정 범위를 지정할 수 있는가? 수정한 결과를 이전 버전과 비교하며 선택할 수 있는가? 채택한 결과를 다음 작업에서 다시 사용할 수 있는가? 이 항목들은 발표된 제품 기능이 아니다. 사용자가 원하는 변경을 표현하고, 변경 결과를 판단하며, 선택을 후속 작업에 반영할 지점을 찾기 위한 질문이다. 피드백을 수집할 때는 만족 여부만 묻기보다 의도와 어긋난 이유, 반복해서 진행이 막힌 단계, 사람이 직접 손질한 부분을 구분하는 접근을 우선할 만하다. 수정 요청이 반복되는 작업에서는 이런 구분이 있어야 개선할 대상을 구체적인 구현 과제로 옮기기 쉽다. 여러 프로젝트에서 반복적인 협업이 이어진다면 연구팀도 단발성 시연에서 발견하기 어려운 작업상의 문제를 접할 기회를 얻는다. 이런 도구의 평가 단위로 제안하는 과정은 다음과 같다. 초안 받기 → 수정하기 → 채택하기 최종 결과물의 품질을 평가할 때, 사용자가 어느 단계에서 진행을 멈추거나 직접 개입했는지도 함께 기록하는 방식이다. 창작자의 수정이 필요한 제품이라면 채택까지 이어지는 과정 자체가 검토 대상이 된다. 반복 요청을 처리할 운영 환경의 조건 일반적인 창작 작업과 인프라의 관계를 표현한 개념도다. 상단의 화살표는 특정 제작 절차를 지정하지 않는다. 운영 조건을 살펴볼 별도 사례로는 ITWorld의 엠키스코어 AI 인프라 구축 관련 기사 가 있다. ITWorld는 엠키스코어의 설명을 인용해, 2025년 NHN과 수행한 NIPA 프로젝트에서 약 957대의 GPU 서버를 세 개 클러스터로 구성했다고 전한다. 이 구축 사례는 DeepMind·A24 협력과 별개이며, 소개된 협력 발표 내용에는 해당 협력의 인프라 구성이 나오지 않는다. 인터뷰에서 강조하는 조건은 서버 사이의 통신 품질과 실행 환경을 함께 맞추는 일이다. 통신을 검증하면서 운영체제, GPU 라이브러리, 컴파일 환경까지 구성해야 한다는 설명이다. 수랭도 냉각수의 유량과 압력만 따로 정하는 문제가 아니라 배관·서버·전력이 맞물려 작동하는 시스템으로 다룬다. 이를 창작 도구의 검증에 적용할 때는 사용자의 반복 수정이 조건이 된다. 수정 요청마다 생기는 대기 시간과 실패가 작업 경험에 영향을 주므로, 모델 실행 여부에 더해 실제 작업을 끝까지 처리하는 동안 각 단계가 어떻게 동작하는지 평가에 포함하는 것이 이 글의 제안이다. 성능 목표는 사용할 작업을 먼저 정한 뒤 설정한다. 대규모 클러스터 사례의 장비 규모를 그대로 제품의 요구사항으로 삼을 이유는 없다. 장비 확장 계획에서는 비용 가정도 갱신하기 자체 장비를 늘리는 계획이라면 GPU뿐 아니라 메모리와 저장장치의 견적도 다시 살펴볼 대상이다. GeekNews의 「메모리 기업들이 소비자 시장을 파괴했다」 는 GamersNexus가 조사한 소비자 제품 표본의 가격 변화를 요약한다. 해당 게시물이 소개한 평균 가격은 다음과 같다. 제품 표본 2025년 9월 평균 가격 2026년 8월 평균 가격 32GB DDR5-6000 CL30 키트 122.50달러 567.50달러 2TB NVMe SSD 143.25달러 340달러 이 수치는 GamersNexus의 조사 자체가 아닌 GeekNews의 요약을 통해 소개된 것으로, 특정 소비자 제품 표본에 한정된다. 서버용 메모리 전반이나 모든 지역의 구매 가격으로 확대해서 적용할 수 없다. 자체 장비 확장에 오래된 견적을 사용하고 있다면, 필요한 메모리와 저장장치의 실제 구매 견적으로 비용 가정을 갱신하는 근거로 삼는 편이 적절하다. 같은 게시물은 제조사의 장기공급계약 확대와 함께 향후 NAND 공급에 대한 전망이 엇갈린다고 전한다. 이 내용으로 가격의 지속 상승이나 클라우드의 비용 우위를 단정할 수는 없다. 자체 구축과 외부 서비스 이용을 비교하는 경우에는 필요한 용량과 이용 시간을 동일한 작업 조건에 맞추고, 각 방식의 실제 견적을 대조해야 비용 판단의 기준이 맞는다. 도입 판단의 우선순위 창작 AI를 도입하려는 팀은 사용자가 결과를 채택하기까지 필요한 수정 과정을 먼저 정의하고, 그 작업을 기준으로 운영 조건과 비용을 함께 평가하는 일을 우선할 만하다. 원문: webi 기술 블로그 참고한 자료: Google DeepMind and A24 announce first-of-its-kind research partnership ‘수랭 없이는 다음 세대 GPU도 없다’ 엠키스코어로 시작하는 AI 인프라 구축 전략 메모리 기업들이 소비자 시장을 파괴했다