Next.js2025년 10월 21일5분 읽기

Flask + Vanilla JS 백오피스를 Next.js로 다시 짠 이유 — 재구축 결정 기준

돌아가는 코드를 왜 건드렸나. 기능 추가가 불가능한 시점을 어떻게 판단했고, 왜 점진적 개선이 아닌 전면 재구축을 선택했는지.

#Next.js#TypeScript#리팩토링#레거시#의사결정#Frontend

Flask + Vanilla JS 백오피스를 Next.js로 다시 짠 이유 — 재구축 결정 기준


상황

사내 테크니컬 라이터들이 릴리즈 노트와 공지 메일을 작성하는 백오피스가 있었습니다. 리치텍스트 에디터, 마케팅팀용 이메일 HTML 빌더 등 여러 툴이 하나의 프로젝트 안에 묶여 있었고, Flask + Vanilla JS로 만들어진 시스템이었습니다.

그런데 이 백오피스에서 간단한 수정 요청이 들어올 때마다 개발이 멈추는 상황이 생겼습니다. 단순히 버튼 색상을 바꾸거나 입력 필드를 하나 추가하는 일도 어려웠습니다.


무엇이 문제였나

코드를 들여다보면 바로 보였습니다.

python
# Flask app.py (단순화)
@app.route('/editor')
def editor():
    return render_template('editor.html')
 
# editor.html 안에 수천 줄의 Vanilla JS
html
<!-- editor.html -->
<script>
  // 2000줄짜리 에디터 로직
  var editorState = {};
  function initEditor() { ... }
  function handleInput(e) { ... }
  function saveDocument() {
    // 여기서 이메일 빌더 로직도 얽혀있음
    ...
  }
  // ...
</script>

문제가 세 가지였습니다.

첫째, 코드 경계가 없었습니다. 에디터 로직, 이메일 빌더 로직, API 호출 코드가 하나의 HTML 파일 안에 전부 들어있었습니다. 에디터를 수정하다 이메일 빌더가 깨지는 경우가 생겼습니다.

둘째, 전체 재빌드가 강제됐습니다. 에디터 하나를 수정해도 전체 Flask 앱을 재배포해야 했습니다. 잦은 수정 요청이 있는 환경에서 이건 실질적인 개발 속도 저하였습니다.

셋째, 인메모리 저장 구조의 위험이 있었습니다. 일부 임시 데이터를 서버 메모리에 저장하는 구조가 남아있었는데, 서버가 재시작되면 사라질 수 있는 데이터였습니다.


왜 점진적 개선이 아니라 전면 재구축이었나

처음엔 점진적 개선을 먼저 검토했습니다. "에디터 부분만 컴포넌트화하자", "이메일 빌더 로직만 분리하자" 같은 접근입니다.

그런데 이 코드에서 점진적 개선은 현실적으로 어려웠습니다.

코드가 너무 깊게 얽혀 있었습니다. 에디터 상태가 이메일 빌더 함수에서 직접 참조되고, 저장 로직이 두 기능에 걸쳐 공유되는 방식이었습니다. "에디터 부분만 분리"하려면 이 의존성을 전부 추적해야 했는데, 그 비용이 처음부터 다시 짜는 것과 거의 같았습니다.

프레임워크가 맞지 않았습니다. Flask는 서버사이드 렌더링 기반이고, 리치텍스트 에디터 라이브러리들은 React 친화적으로 발전하고 있었습니다. 기존 구조를 유지하면서 최신 에디터 라이브러리를 붙이기가 어려웠습니다.

재구축 범위가 크지 않았습니다. 이 백오피스의 사용자는 사내 일부 팀원들로 한정됐고, 기능도 명확했습니다. 외부 서비스처럼 복잡한 마이그레이션이 필요 없었고, 재구축 후 사용자에게 변경 사항을 공지하는 것도 어렵지 않았습니다.

이 세 가지를 고려하면 점진적 개선이 오히려 더 비효율적이었습니다.


기술 스택 선택

Next.js + TypeScript를 선택한 이유

"유행하니까"가 아니라 실제 필요에 맞았기 때문입니다.

  • 리치텍스트 에디터 라이브러리(TipTap, Slate 등)가 React 기반이었습니다. React를 선택하면 생태계가 풍부했습니다.
  • TypeScript는 사내 툴이라 빠르게 짜고 잊어버리는 경우가 많은데, 타입이 있으면 나중에 돌아와서 코드를 볼 때 맥락을 파악하기가 훨씬 편합니다.
  • Next.js는 API 라우트를 제공해서 Flask 백엔드를 점진적으로 대체하면서 배포 단위를 하나로 통합할 수 있었습니다.

GCP Cloud Run을 선택한 이유

사내 인프라가 GCP 기반이었고, Cloud Run은 컨테이너를 서버리스로 실행할 수 있어서 트래픽이 적은 내부 툴에 비용 효율적이었습니다.


재구축 과정에서 중요하게 생각한 것

모듈 경계 설계

각 툴을 독립 모듈로 분리하는 게 핵심 목표였습니다.

text
src/
├── modules/
│   ├── release-editor/     # 릴리즈 노트 에디터
│   │   ├── components/
│   │   ├── hooks/
│   │   └── api/
│   ├── email-builder/      # 이메일 HTML 빌더
│   │   ├── components/
│   │   ├── hooks/
│   │   └── api/
│   └── announcement/       # 공지 관리
└── shared/                 # 공통 컴포넌트

에디터를 수정해도 이메일 빌더 코드에 영향이 없고, 각 모듈을 독립적으로 배포할 수 있게 됐습니다.

데이터 저장 구조 정리

인메모리에 있던 데이터는 Cloud SQL(MySQL)로 이전했습니다. 트랜잭션과 백업이 보장되는 구조로 바꿨습니다.

typescript
// 이전: 서버 메모리에 직접 저장
let editorDrafts: Record<string, string> = {};
 
// 이후: DB에 명시적으로 저장
await db.drafts.upsert({
  where: { userId_type: { userId, type: 'release-note' } },
  create: { userId, type: 'release-note', content },
  update: { content, updatedAt: new Date() },
});

인증 추가

재구축 기회에 인증도 함께 도입했습니다. 기존엔 내부망에서 누구나 접근 가능한 상태였는데, 사내 OAuth + JWT로 RBAC(역할 기반 접근 제어)를 구성했습니다.

typescript
// 미들웨어로 역할 검증
export function withRole(requiredRole: Role) {
  return function(handler: NextApiHandler): NextApiHandler {
    return async (req, res) => {
      const user = await getAuthUser(req);
      if (!user || !hasRole(user, requiredRole)) {
        return res.status(403).json({ error: 'Forbidden' });
      }
      return handler(req, res);
    };
  };
}
 
// 사용 예시
export default withRole('technical-writer')(async (req, res) => {
  // 테크니컬 라이터만 접근 가능
});

결과

3개월에 걸쳐 재구축을 완료했습니다.

항목변경 전변경 후
기능 수정 시 영향 범위전체 앱해당 모듈만
배포전체 재배포모듈별 독립 배포 가능
코드 파악 시간수천 줄 단일 파일모듈별 분리
서버 재시작 시 데이터유실 위험DB 저장으로 안전
접근 제어없음RBAC

에디터 수정 요청이 들어왔을 때 "이거 건드리면 다른 게 깨질 수 있어서..." 라고 말하지 않아도 됐습니다.


마치며

재구축 vs 점진적 개선을 결정할 때 저는 이 세 가지를 봅니다.

  1. 코드 결합도: 독립적으로 분리 가능한가, 아니면 너무 깊이 얽혀있는가
  2. 재구축 범위: 사용자가 얼마나 되고, 마이그레이션 비용이 현실적인가
  3. 기술 적합성: 현재 프레임워크로 앞으로 필요한 기능을 구현할 수 있는가

세 가지가 모두 재구축 쪽을 가리킬 때는 과감하게 새로 짜는 게 낫습니다. 점진적 개선이 항상 안전한 선택은 아닙니다. 무너져가는 구조 위에 기능을 계속 쌓다 보면 나중에 훨씬 더 큰 비용을 치르게 됩니다.

"돌아가는 코드를 건드리지 말라"는 말은 결합도가 낮고 테스트가 충분할 때의 이야기입니다.

#Next.js#TypeScript#리팩토링#레거시#의사결정#Frontend

황호민

Backend Engineer · Java/Kotlin · Spring Boot · Next.js