← 블로그

· 크래프톤 정글, 회고

정글 돌아보기 2: 나만무

나만무 회고 — Stolink

크래프톤 정글 마지막 5주. 나만의 무기를 만드는 시간.

우리 팀은 "Stolink"를 만들었다. 소설 작가가 자기 글을 체계적으로 관리하고, 등장인물 관계를 시각화하고, AI로 서사 정합성을 검증받을 수 있는 플랫폼.

5인 팀. 풀스택 2, 인프라 1, AI 서버 1, 팀장(풀스택 겸임). 나는 풀스택을 맡았다.

분류스택
FrontendReact, TypeScript, Canvas (D3.js)
BackendSpring Boot, JPA (Hibernate), Redis, RabbitMQ
DatabasePostgreSQL, Neo4j (Graph DB)

왜 이걸 만들었는가

기획 마지막 날이었다. 두 개 중 하나로 정하기로 했고, 2시부터 3시까지 딱 한 시간, 각자 하나씩만 떠올리기로 했다. 그때 나온 아이디어였다.

30분 만에 초안을 설명해야 해서 횡설수설했는데, 머릿속에 메인 컨셉은 잡혀있었다. 이야기하면서 조금씩 명확해졌고, 팀원들 반응도 좋았다. 재밌을 것 같다, 기술적 난이도도 적당하다, 붙여볼 만한 게 많다, 다들 그렇게 말했다.

우리 팀의 다른 주제는 축제 주점 플랫폼이었다.

그 전에 내가 구상했던 건 따로 있었다. 우버를 레퍼런스로 한 초단기 알바 플랫폼. 근데 서비스적으로 치명적인 문제가 있었다. 노쇼하거나 불성실한 블랙 컨슈머를 제재할 방법이 없었다. 업장은 이미 피해를 봐버리니까. 그래서 기각.

Stolink이 살아남은 이유는 명확했다. 작가가 쓴 글을 그대로 분석해서 챗봇도 붙이고, 에디터 자체로도 여러 요소를 통해 체계적인 스토리 진행에만 집중할 수 있게 해준다는 컨셉. 시각적 요소랑 기술적 요소 전부 우리가 할 수 있는 수준이라고 생각했다. 사실 도전적이었다.

"이건 된다" 싶었던 건 에디터 구현, 받아온 데이터로 그래프 예쁘게 띄우기, 그리고 결제. Toss MCP 아티클을 토스 기술 블로그에서 읽어서 연동 가능하다고 봤다.

"이건 좀 무리 아닌가" 싶었던 건, 소설 자체에서 등장인물, 관계, 사건을 파싱하는 부분이었다. 팀원 한 명이 전담했는데, 만약 등장인물이 사람에서 슬라임으로 바뀌면? 소설에서는 무슨 일이든 일어날 수 있잖아. 등장인물이 1000명씩 나오는 소설이라면? 이걸 다 트래킹할 수 있나?


실제로 만든 것

프론트엔드

유저 플로우를 고려해서, 랜딩 페이지 → 체험 가능한 데모 페이지 → 로그인 → 에디터 화면 → 사이드바 → 데이터 오버뷰가 가능한 월드 페이지까지의 흐름을 주로 맡았다. 다른 팀원이 디버깅을 잡아주는 동안 나는 화면 쪽에 집중할 수 있었다.

이전 프로젝트에서는 React를 써도 HTML 쓰듯이 코드를 짰더라. 부끄럽지만 상태 관리라는 개념 자체를 제대로 모르고 있었다. 이번에 zustand, TanStack Query를 처음 써보면서 트러블을 많이 겪었고, 그 과정에서 조금씩 감을 잡아갔다.

백엔드

인증 쪽은 JWT를 맡았다. 이전에 해본 적이 있어서 비교적 수월했는데, 끝날 즈음에 OAuth2를 붙여야 할 이유가 생겼다. "Storead"라는 독자용 플랫폼이 기획에 추가되면서, SSO로 한 아이디로 두 사이트를 오갈 수 있어야 했다. 서비스적으로도 구글 로그인은 이미 보편화됐고, UX 서적에서 읽었던 것처럼 회원가입 같은 이탈 포인트는 배제하는 게 맞았다.

결제 로직도 담당했는데, 이 부분은 솔직히 자신이 없었다. 서비스의 가치가 훼손되지 않으려면 정합성이 중요한데, 경험이 부족하다 보니 AI한테 계속 안티 패턴을 물어보면서 하나씩 테스트를 돌렸다. 부족한 만큼 더 꼼꼼하게 검증하려 했다.


가장 어려웠던 것 — 그래프 렌더링

Stolink의 핵심 기능 중 하나가 소설 인물 관계도였다. D3.js 기반 SVG로 구현했는데, 처음에는 잘 돌아갔다. 문제는 인물이 많아지면서부터였다.

노드가 50개만 넘어도 클릭이나 드래그 반응이 눈에 띄게 느려졌다. 관계선이 그물처럼 촘촘해져서 캐릭터를 찾기도 힘들고, 화면 자체가 버벅대니까 "이걸 왜 쓰지?" 싶은 수준이었다. 관계도라는 기능의 존재 의미가 흔들리는 순간이었다.

원인 — SVG의 구조적 한계

처음엔 원인을 몰랐다. 그냥 노드가 많아서 느린 건가 싶었는데, DevTools Performance 탭을 열어보니 Style Recalculation이 병목이었다. SVG는 각 노드가 개별 DOM 요소로 존재하기 때문에, 노드 수가 늘어날수록 브라우저가 매 프레임마다 수백 개의 요소를 재배치(Reflow)해야 한다. 구조적인 한계였다.

전환 — Canvas API

그래서 Canvas로 전면 전환하기로 했다. 수백 개의 DOM 요소 대신 단일 Canvas 엘리먼트 위에 전부 그리는 방식. 캐릭터 이미지는 Offscreen Canvas에 비트맵으로 캐싱해놓고, 단일 렌더 루프에서 일괄 렌더링했다.

근데 Canvas로 갈아타니까 또 다른 문제가 생겼다. DOM이 아니니까 클릭, 드래그 같은 이벤트를 쓸 수가 없다. 좌표 기반 히트 테스트로 직접 구현해야 했다.

React의 불변성(Immutability)과 물리 엔진의 가변성(Mutability)이 충돌하는 문제도 있었는데, structuredClone으로 물리 연산용 복사본을 따로 만들고 useRef로 실시간 위치를 참조하는 식으로 분리했다.

// 물리 연산용 복사본 분리 — React 원본은 보호
const graphData = useMemo(
  () => ({
    nodes: structuredClone(initialNodes),
    links: structuredClone(processedLinks),
  }),
  [initialNodes, processedLinks],
);

// 매초 수십 번 변하는 좌표는 useRef로 — 리렌더링 방지
const graphDataRef = useRef(graphData);

UX — 보기 쉬운 관계도 만들기

성능 말고도, 어떻게 하면 복잡하지 않고 사용자가 보기 쉽게 띄울지도 계속 고민했다. 노드가 많아지면 관계선이 겹쳐서 스파게티가 되는데, 이걸 어떻게 풀지가 진짜 머리 아팠다.

여러 가지를 시도했는데, 결국 가장 효과적이었던 건 간선을 직선이 아니라 끝으로 갈수록 얇아지는 아크(곡선)로 바꾼 것이었다. 직선일 때는 선이 겹치면 구분이 안 됐는데, 아크로 바꾸니까 시각적으로 훨씬 깔끔해졌다. 거기에 관계 타입(우호, 적대, 가족 등)을 색으로 구분해서 넣으니까, 선만 봐도 두 캐릭터가 어떤 관계인지 직관적으로 파악할 수 있게 됐다.

쉽지 않았다. Canvas API 자체도 처음이었고, 물리 엔진이랑 React의 데이터 흐름을 동시에 다뤄본 적도 없었다. 한참을 헤매면서 D3.js 관계 그래프 구현 사례도 찾아보고, 브라우저 렌더링 파이프라인도 공부했다.

그렇게 며칠을 붙잡고 나서 측정해보니 INP 420ms → 64ms, FPS 10 미만 → 60. 노드 650개를 올려도 끊김 없이 돌아갔다. 진짜 되네. 팀원들한테 보여주니까 이제야 완성 단계에 이른 것 같다는 반응이었다. 그 말 듣고 나서야 며칠간의 피로가 좀 풀렸다.


못 만든 것들

많다. 진짜 많다.

스튜디오 — 작가가 특정 단락을 드래그해서 요청하면 그게 프롬프트로 날아가서 쇼츠를 만들고, 유튜브에 배포하는 기능. 팀원들과 같이 페이지와 기능까지는 구현했는데, Veo3의 비디오 생성이 8초에 700원이라 비용 문제로 기각됐다.

서사 분석 — 소설의 진행에 따른 서사를 기승전결로 분석해서 시각화하거나, 이미지 생성 시 캐릭터가 가진 장비나 아이템 같은 소품을 반영하고, 인물의 능력치를 스탯화해서 보여주는 기능. 글 분석 에이전트에 이것저것 태우다 보니 과부하가 걸렸다. 분석 시간이랑 비용이 감당이 안 돼서, 경량화하는 과정에서 결국 빠졌다.

idle 애니메이션 — 캐릭터 이미지가 정적이 아니라 시선을 돌리거나, 바람에 머리가 흩날리는 정도의 움직임. 이게 있었으면 발표 때 임팩트가 확 달랐을 것 같다. 근데 비용도 문제였고, AWS 기능을 따로 쓰든 이미지 서버를 스케일 업해서 파이프라인에 모델을 추가하든 해야 하는데, 남은 시간이 부족해서 빼야 했다.


팀으로 일한다는 것

노션을 주로 쓰고, 단톡에 머지나 공유 사항을 바로바로 알렸다. GitHub 이슈 관리도 했다.

반복되는 커밋/PR 작성이 병목이라는 걸 파악하고, Antigravity의 커스텀 커맨드 기능으로 이슈-브랜치-PR 파이프라인을 단일 자동화했다. 불필요한 컨텍스트 스위칭을 줄여서 기획/개발에 집중할 수 있었고, 다른 팀에도 전파했다.

가장 좋았던 점 — 큰소리가 나거나 싸운 적이 없다. 진행 속도가 빨랐던 덕에 프로젝트 볼륨을 늘려 다채롭게 만들 수 있었다. 다들 열심히 해준 덕분이다.

아쉬웠던 점 — 기능 구현에만 급급해서 고도화를 나중에서야 챙겼다. 고도화 과정에서 좋은 아이디어가 많이 떠올랐는데, 더 일찍 시작했으면 단편적인 기능이 아니라 유기적으로 엮인 좋은 기능들을 더 많이 만들 수 있었을 것 같다.


발표와 피드백

발표 때 조작을 맡았다. 플로우를 잘 아는 사람이 하는 게 낫겠다는 팀원들의 판단이었고, 정확한 흐름으로 진행되도록 합을 많이 맞췄다. 보여주고 싶은 부분을 최대한 매력적으로 보여줄 수 있도록 팀 전체가 신경 썼다.

받은 피드백 중 기억나는 것 — UI가 뛰어나다, UX적으로 여기까지 생각했냐는 반응, 서비스 기능의 폭이 넓다는 말들. 팀 전체가 열심히 만든 결과물이 인정받는 느낌이라 기분이 좋았다.

동석 코치님은 웹소설을 잘 모르셔서 논문 작성 툴로 생각하시더라. 그걸 보면서, 타겟층에 따라 매력적으로 느끼지 않을 수도 있겠다는 생각은 했다.

다른 팀 중에서는 설문조사 팀이 인상 깊었다. 기능도 많고 잘 만들었다. 소담이도 재밌었고, 옷 입히기 프로젝트는 팀원 리소스 배분이 잘 돼서 완성도 높은 기능 구현이 가능했던 것 같다.


아쉬움

시간이 더 있었으면 문서 정리를 제대로 했을 것 같다. 문서화를 신경 썼다고 생각했는데, 정작 정리는 안 했다. 너무 많은 문서가 오히려 병목이 됐다. 에이전트에 모든 문서를 맡기려면 내가 문서에 대한 검토 시간이 충분히 필요했는데, 내가 기억하는 내용이 문서에는 없더라. 컨텍스트 압축 과정에서 날아간 것 같다.

다 끝났다고 생각하고 중간에 놀던 2~3일이 있었다. 미처 체크하지 못한 부분에서 버그가 터졌다. 다른 팀원에게 부탁을 하든, 내가 테스트 코드를 더 꼼꼼히 작성해서 돌렸어야 했다.


정글이 남긴 것

솔직히 프론트를 이렇게 많이 해본 건 처음이었다. 그 전까지는 상태 관리라는 개념 자체를 제대로 모르고 있었다는 걸 이번에야 깨달았다. zustand, TanStack Query를 써보면서 캐싱 문제, 상태 동기화 문제를 직접 겪었고 , 아직 잘한다고 말하기엔 부족하지만, 적어도 뭘 모르는지는 알게 됐다.

기술 외적으로 배운 건 내가 이 정도까지 몰입할 수 있구나. 진짜 개발이 좋은 건지 헷갈렸는데, 살면서 게임도 이렇게 해본 적이 없는 것 같다. 잘하는지는 모르겠지만, 진짜 열심히 했다.

정글 전체를 돌아봤을 때 가장 기억에 남는 건 아침이다. 아침에 일어난 적은 별로 없고, 항상 밤샌 뒤였다. 밤새 다른 팀원과 개발 이야기를 하거나, 살아온 인생에서 느낀 가치에 대해 이야기하거나, 밤새 코딩하며 머리 싸매다가 바깥 창문이 파래지던 기억.

한 달밖에 안 지났는데도 모두가 각자의 길을 가고 있다. 누군가는 취업에 성공했고, 나처럼 준비하는 사람도 있다.

솔직히 지금 힘들다. 고시원에서 살면서 쿠팡 뛰는 삶이 녹록치 않다.

하지만 정글을 가길 잘했다고 백 번 생각한다. 개발자라는 꿈이 학과가 컴공이라서 그냥 흘러가는 대로가 아니라 진짜 잘하는 개발자가 되어보고 싶다는 생각이 확고하게 들었다. 어떤 걸 준비해야 하는지, 내가 같은 취준생에 비해 뭐가 부족한지 확실히 알게 됐다. 좋은 동료들도 만났고, 지금의 여자친구도 만났다.

더 열심히 살아서 10년 뒤에는 실력이 뛰어난 개발자가 되고 싶다. 정년까지 열심히 개발하고, 은퇴 후에는 기술로 세상을 이롭게 하며 살고 싶다. 봉사도 많이 다니고, 삶의 보람을 느끼면서 내가 살아온 삶의 가치를 올리고 싶다.

그러려면 개발 실력이 꼭 필요하다. 그래서 지금의 공부가 전혀 힘들지 않다. 지금의 삶도 마찬가지다.