
10여 년 만에 다시 만난 외주 프로젝트 리뉴얼 하기
"코드 거지 같이 짰네. 이거 누가 짰냐?" "너요."
- 유형 · 웹 서비스·앱
- 사용 도구 · Claude Code
- 만든 방식 · 둘 다 섞어서
- 제작 기간 · 주말 하루
만든 과정
대학 시절 아르바이트로 만든 웹사이트가 하나 있습니다. 10여 년 뒤 리뉴얼을 맡아 그 코드를 다시 열어본 저는 클로드에게 넘기며 이렇게 입력했습니다.
"코드 거지 같이 짰네. 이거 누가 짰냐?"
클로드가 뒤적거리더니 말했습니다.
"너요."
어떤 문제를 풀었나요
처음 이 웹사이트는 딱 한 가지 목표를 중심으로 준비했습니다. 운영진이 관리하기 쉬울 것.
그러다보니 10년이 넘도록 추가적인 유지 보수도 진행되지 않았습니다. 그러며 이런 문제가 생겼습니다.
- WordPress 3.5.1, MySQL 5.1, MyISAM 테이블, utf8 인코딩. REST API도 없었습니다.
- Twenty Twelve 테마를 직접 고쳐 쓰고 있었고, 배경 슬라이드(jQuery vegas)와 작품 슬라이더(AnySlider)는 글 본문 안에 스크립트로 들어 있었습니다.
- 작품 하나가 국문 글과 영문 글로 따로 있었습니다(85쌍). 당시에는 관리하기 쉬우라고 나눴는데, 결국 고칠 일이 생기면 글 두 개를 같이 고쳐야 했습니다.
- 위치·구조·면적 같은 제원은 본문 HTML 속 표에 섞여 있었습니다. 규격화된 데이터처럼 보였지만 실제로는 글자일 뿐이어서, 관리자가 고치려면 HTML을 직접 만져야 했고 프로그램으로 꺼내 쓸 수도 없었습니다.
- 글 리비전 1,120개, 지금은 쓰지 않는 플러그인 설정이 쌓여 있었습니다.
때마침 터진 PHP2Shell
리뉴얼 의뢰를 받고 작업을 시작한 게 10월 19일이었습니다. 이틀 뒤인 10월 21일, PHP2Shell이 공개됐습니다.
PHP2Shell은 PHP 런타임 자체에서 나온 치명적 취약점 두 건(CVE-2026-109618, CVE-2026-109619)을 묶어 부르는 이름입니다. 로그인 없이 원격에서 서버 코드를 실행할 수 있고, CVSS 4.0 점수는 9.2입니다. 하나는 세션 업로드 진행 상황 처리에서 생기는 메모리 해제 후 사용(use-after-free) 문제이고, 다른 하나는 메모리 부족 오류를 처리할 때 생기는 메모리 손상 문제입니다. PHP로 웹 요청을 처리하는 서버는 전부 대상이라서, WordPress나 플러그인만 업데이트해서는 막을 수 없고 PHP 자체를 올려야 합니다. 패치 버전은 8.5.12, 8.4.27, 8.3.36, 8.2.35이고, 8.1 이하는 지원이 끝나 패치도 없습니다. KISA도 10월 22일 대응 수칙을 내고, 업데이트 전에 인터넷에 노출됐던 서버는 이상이 없어 보여도 DB 비밀번호 같은 인증정보를 바꾸라고 권고했습니다.
13년 전 WordPress 3.5.1을 돌리던 옛 서버는 당연히 패치 대상조차 아니었습니다. 때마침 의뢰가 들어와 작업을 시작한 참에 사건이 터졌으니, 대응은 오히려 편했습니다. 이미 옛 서버를 백업하고 새 환경으로 옮길 준비를 하던 중이라, 디자인만 바꾸는 게 아니라 서버 사양부터 올린다는 계획에 PHP2Shell 대응을 그대로 얹으면 됐습니다.
리뉴얼 전에 해킹 흔적부터 점검했습니다. 업로드·이미지 폴더에 PHP 파일이 없는지, DB 본문에 악성 코드가 없는지, 모르는 관리자 계정이 없는지 확인했습니다. 다행히 흔적은 없었습니다.
클라이언트의 요구는 세 가지였습니다.
- 기존 사이트의 인상(검은 바탕, 큰 배경 사진, 작품 썸네일 격자)은 그대로 두기
- 메인 화면을 사진 슬라이드 대신 영상으로 바꾸고, ARCHIVE 메뉴를 새로 만들기
- 재단 직원이 지금처럼 WordPress 관리자 화면에서 글과 작품을 고칠 수 있게 하기
그리고 제가 따로 정한 조건이 하나 있었습니다. 10년 넘게 쌓인 주소와 데이터를 하나도 잃지 않을 것. 검색엔진과 다른 사이트에 걸린 옛 링크(/archives/1234 같은 주소)가 깨지면 안 됐습니다.
어떻게 만들었나요
1. 화면은 React, 콘텐츠는 WordPress
React(Vite)로 만든 화면을 WordPress 테마로 빌드하는 하이브리드 구조를 골랐습니다.
npm run build결과가 테마 폴더(theme/dist/)에 들어가고, PHP 템플릿은 글 본문을<template>태그에 넣어 둡니다. React가 그걸 읽어서 화면을 그립니다.- 별도 API 서버나 headless 구성 없이 공유 호스팅(Cafe24)에 테마 하나만 올리면 끝나는 구조라서, 재단 쪽 운영 부담이 늘지 않습니다.
- 관리자 화면은 그대로 WordPress입니다. 작품 제원·영문 본문·배경 이미지는 ACF 입력 칸으로 나눴습니다.
2. 데이터 이전은 "몇 번을 돌려도 같은 결과"로
옛 DB를 손으로 고치지 않고, 원본 덤프에서 처음부터 다시 만드는 스크립트를 짰습니다. 로컬 Mac에 Colima + Docker로 스테이징 WordPress를 띄우고, 명령 하나로 아래 과정이 순서대로 돕니다.
| 단계 | 내용 |
|---|---|
| 기술 정비 | 테이블 접두어 변경, utf8 → utf8mb4, MyISAM → InnoDB, DB 구조 3.5 → 7.1 |
| 정리 | 리비전 1,120개, 옛 플러그인 데이터 49건, 임시 캐시 삭제 |
| 작품 | 국/영문 85쌍 → 작품 85개로 합치고 제원을 입력 칸으로 분리, 연대 분류 지정 |
| 페이지 | 국문·영문 페이지를 하나로 합치고(?lang=en), 본문 속 스크립트·인라인 스타일 제거 |
| 뉴스 | NEWS 카테고리 글 14개를 뉴스 콘텐츠로 이전 |
| 주소 | 옛 주소 172개 → 새 주소 대응표, 테마가 301로 넘겨 줌 |
원본은 절대 건드리지 않고, 스크립트를 고칠 때마다 처음부터 다시 돌려서 결과를 확인했습니다. 덕분에 "어디까지 손으로 고쳤더라?"를 기억할 필요가 없었습니다.
3. 옛 짐 덜어내기
- vegas·AnySlider를 걷어내고 화살표·도트 무늬는 CSS로 그렸습니다.
- 배경 사진 5장은 21MB → 1.9MB로 줄여 테마에 넣었습니다.
- 가독성도 손봤습니다. 글자 13~15px, 반투명 바탕, 현재 메뉴 표시, 작품 목록의 연대 버튼(모바일은 연대 버튼만).
4. 메인 배경 영상
재단에서 받은 원본은 4K 60p 로그 촬영 영상 6개, 합계 7.2GB였습니다. 웹에 그대로 올릴 수는 없어서 ffmpeg로 720p·30fps·흑백·무음으로 바꿨습니다. 로그 촬영 특유의 뿌연 화면은 영상마다 밝기 범위를 측정해 보정값을 따로 계산했고, 결과는 6개 합계 약 17MB입니다. 메인 화면에서 무작위 순서로 돌아가고, 보정값과 명령은 다시 만들 수 있게 문서로 남겼습니다.
5. 서버 사양 업데이트와 운영 서버 이전
PHP2Shell에 대응하려고 서버를 통째로 바꿨습니다.
| 이전 | 이후 | |
|---|---|---|
| WordPress | 3.5.1 (2013) | 7.1.3 |
| DB | MySQL 5.1, MyISAM, utf8 | MariaDB 10.6, InnoDB, utf8mb4 |
| PHP | 옛 버전 | 8.4.27 (PHP2Shell 패치 버전) |
| 플러그인 | Jetpack 등 5개 | ACF 하나 |
옛 서버에 덧대어 업데이트하지 않고, 전체를 백업한 뒤 Cafe24 서버를 PHP 8.4 / MariaDB 10.6으로 초기화해서 새로 받았습니다. 옛 파일은 그대로 옮기지 않고, 새 테마와 업로드 파일만 올렸습니다. 혹시 숨어 있을지 모를 파일까지 함께 넘어오지 않게 하려는 것입니다. 서버 접속도 SSH 키 방식으로 바꿨습니다. 공유 호스팅 셸에서 wp db import가 동작하지 않아, PHP mysqli로 SQL을 넣는 작은 도구를 만들어 썼습니다. 이전 뒤에는 주요 페이지 200 응답, 옛 주소 301, 이미지 표시를 하나씩 확인했습니다.
6. AI 코딩 도구와 같이 작업
이번 작업은 Claude Code와 함께 했습니다. 기존 사이트 데이터 수집, 이전 스크립트 작성, ffmpeg 보정값 계산 같은 반복 작업을 맡기고, 저는 구조를 정하고 결과를 확인하는 쪽에 집중했습니다. 작업 기록(docs/progress.md)과 README도 작업하면서 같이 정리해 두었습니다.
내 역할
개발 및 리뉴얼
댓글 0
첫 댓글을 남겨 보세요
응원이나 질문 한마디면 충분해요.