아무도 문서화하지 않은 프레임워크: Backbone 앱을 React로 전환하기
요약
본 가이드는 Backbone으로 구축된 레거시 앱을 React로 전환하는 방법을 다룹니다. 단순히 라이브러리를 교체하는 것이 아니라, 팀이 자체적으로 구축한 '문서화되지 않은 프레임워크'를 찾아내고 이를 React의 컴포넌트 구조로 대체하는 과정에 초점을 맞춥니다. 단계적 마이그레이션 접근 방식을 제시하며, 테스트 코드 작성 및 불필요한 레거시 코드를 정리하는 방법을 안내합니다.
핵심 포인트
- Backbone 전환은 라이브러리 교체가 아닌 자체 프레임워크 재구축 과정이다.
- 전역 싱글톤, 이벤트 버스 등 팀이 만든 구조를 찾아내고 대체해야 한다.
- React의 컴포넌트화와 테스트 코드를 통해 레거시 코드를 정리할 수 있다.
- 큰 앱은 단계적으로 마이그레이션하는 접근 방식이 필요하다.
Backbone은 모델(models), 뷰(views), 라우터(router)와 이벤트(events)를 제공하고 나머지는 여러분에게 맡겼습니다. 따라서 대부분의 Backbone 앱은 그 위에 자체적인 프레임워크를 가지고 있습니다. 즉, 전역 싱글톤(global singleton), 이벤트 버스(event bus), 네비게이션 컨트롤러(navigation controller), 뷰 정리 코드(view cleanup code), 템플릿 빌드 단계(template build step) 그리고 수많은 jQuery 플러그인들입니다. React로 전환하는 것은 주로 그 프레임워크를 찾아내고 그것을 무엇으로 대체할지 결정하는 과정에 가깝습니다. 이 가이드는 Backbone과 React를 나란히 실행하는 방법, React가 Backbone 모델을 읽도록 하는 방법, 뷰를 컴포넌트(components)로 변환하는 방법, 테스트가 없던 앱을 테스트하는 방법, 그리고 오직 Backbone을 깔끔하게 유지하기 위해 존재했던 코드를 삭제하는 방법을 다룹니다.
Backbone은 처음부터 완전한 프레임워크가 되려고 하지 않았습니다. 모델, 컬렉션(collections), 뷰, 라우터, 이벤트를 제공하고 나머지는 여러분에게 맡겼을 뿐입니다. 뷰가 어떻게 정리되는지, 앱의 한 부분이 다른 부분과 어떻게 통신하는지, 네비게이션이 어디에 존재하는지, 템플릿이 어떻게 빌드되는지: 이 모든 것은 팀에서 결정했습니다. 혹은 매우 흔하게는 회사를 떠난 몇 년 전의 누군가가 결정했을 수도 있습니다.
그래서 Backbone을 떠나는 것이 다른 프레임워크를 떠나는 것과는 다르게 느껴집니다. 여러분은 실제로 Backbone에서 벗어나는 것이 아닙니다. 그 위에 팀이 구축한 프레임워크, 그리고 아무도 문서화하지 않은 바로 그것에서 벗어나는 것입니다. 좋은 소식은 이 모든 것의 상당 부분이 단순히 삭제될 수 있다는 것입니다. 왜냐하면 React가 이미 그러한 작업을 수행하기 때문입니다. 진정한 작업은 먼저 이 모든 것을 찾아내는 것입니다.
소스 코드: 본 게시물의 예제들은 Backbone 1.0, RequireJS, jQuery Mobile 및 Handlebars로 구축한 작은 GitHub 뷰어에서 가져온 것입니다. 데스크톱에서는 뷰들이 패널 형태로 나란히 배치되고, 휴대폰에서는 각각 독립적인 페이지가 됩니다. 저희는 동일한 화면, 요소 ID(element ids) 및 API 호출을 사용하여 React 18로 이를 재구축했습니다. 두 버전은 cobuild-tech/migratex-examples에서 찾을 수 있습니다. 앱이 작았기 때문에 단순히 다시 작성했습니다. 더 큰 앱의 경우, 단계적으로 이동하며, 이것이 본 게시물에서 설명하는 접근 방식입니다.
시작하기 전에
왜 떠나야 하는가, 그리고 언제 그렇지 않은가
저희가 만난 누구도 Backbone에 불만을 가지고 있지는 않습니다. Backbone은 약속한 것을 정확히 이행했습니다. 팀이 떠나는 이유는 그 주변에서 생겨난 모든 것들 때문입니다. RequireJS, Handlebars 1.x 및 오래된 jQuery 플러그인들이 사용되지 않게 되었고, 저희 예시의 경우 jQuery Mobile은 2021년에 자체 유지보수자에 의해 공식적으로 지원 중단되었습니다. 모든 Backbone 앱은 자체적인 규칙을 가지고 있어, 새로운 개발자는 버튼 하나를 변경하기 전에 팀의 특정한 설정을 배워야 합니다. 그리고 거의 모든 Backbone 팀은 페이지에서 제거되었지만 이벤트를 계속 수신하는
| 팀이 구축한 것 | 용도 | 우리 예시에서 |
|---|---|---|
| 전역 싱글톤 (global singleton) | 모든 모듈이 컨트롤러, 이벤트 버스 또는 현재 사용자에게 접근할 수 있도록 함 | Globals, 순환 의존성(circular RequireJS dependency)을 끊기 위해 생성함 |
| ... | ||
여기서 대략적인 크기를 파악하는 빠른 방법은 코드에서 .extend(, listenTo(, .on(, trigger(, $(를 검색하고 전역 객체의 이름을 검색한 후, 발견 횟수를 세는 것입니다. extend 횟수는 모델과 뷰의 개수를 알려줍니다. 이벤트와 jQuery 횟수는 이들 사이에 얼마나 많은 숨겨진 배선 작업(hidden wiring)이 있는지 보여주며, 대부분의 시간이 여기에 쓰입니다. |
먼저 최신 번들러를 사용하도록 전환하세요
만약 앱이 여전히 RequireJS나 긴 <script> 태그 목록을 통해 로드된다면, React 코드가 도착하기 전에 이것부터 변경해야 합니다. 좋은 점은 모듈을 다시 작성할 필요가 없다는 것입니다. webpack은 AMD define() 호출을 이해합니다. 따라서 기존 코드는 그대로 번들링되어 나중에 파일별로 ES 모듈(ES modules) 파일로 이동될 수 있습니다. Vite는 새로운 React 코드에 매우 잘 작동합니다. 이 기회에, 수동으로 실행하던 템플릿 빌드 단계를 번들러 안으로 옮겨서 모든 빌드에서 실행되도록 하세요.
그런 다음 Backbone을 1.4 이상으로 업그레이드하세요. 이것은 오래된 패턴들을 일찍 표면 위로 끌어올리는 작은 단계입니다. 우리 예시에서는 여전히 뷰 내부에서 this.options를 읽었는데, 이는 Backbone이 1.1 버전에서 제거했습니다. 또한, model.on(...)을 this.listenTo(model, ...)로 교체하기 좋은 시기입니다. 이렇게 하면 뷰가 제거될 때 자체 리스너를 정리할 수 있습니다.
Backbone과 React 함께 실행하기
[
한 번에 모든 것을 재작성하는 것이 매력적이지만 위험합니다. 더 안전한 방법은 Martin Fowler의 strangler fig 접근 방식입니다. 즉, 기존 앱 주변에 새로운 코드를 조각조각 추가하여, 결국 기존 앱이 필요 없어질 때까지 진행하는 것입니다. Backbone은 이 과정을 거의 모든 다른 프레임워크보다 쉽게 만듭니다.
Backbone 뷰 내부에 React 마운트하기
Backbone 뷰는 기본적으로 DOM 요소를 담고 있는 객체일 뿐이므로, 해당 요소를 createRoot를 사용하여 React에 바로 전달할 수 있습니다:
// views/cartView.js
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
...
나머지 Backbone 앱은 아무것도 변경된 것을 감지하지 못합니다. 여전히 CartView를 생성하고 이전에 하던 대로 render()와 remove()를 호출합니다. render()를 다시 호출하는 것은 단순히 새로운 props로 React 컴포넌트를 업데이트할 뿐입니다. 다만, 뷰와 함께 React root가 정리되도록 반드시 remove()를 오버라이드해야 한다는 점을 기억하십시오.
모델 및 URL 공유하기
한동안 React 컴포넌트는 여전히 Backbone 모델에 존재하는 데이터가 필요할 것입니다. 이 데이터를 React state로 복사하여 두 버전을 동기화할 필요는 없습니다. Backbone 모델은 이미 change 이벤트를 발생시키며, 이것이 바로 React의 useSyncExternalStore 훅이 필요한 모든 것입니다:
export function useModelValue(model, key) {
model.on(`change:${key}`, onChange);
...
이제 Backbone 뷰는 cart.set('count', 3)를 호출할 수 있고 React 컴포넌트는 스스로 업데이트됩니다. 이 과정을 원활하게 유지하는 간단한 규칙이 있습니다. 속성(attribute)에 객체나 배열을 담고 있는 경우, 제자리에서 변경하기보다는 항상 새로운 값으로 set해야 합니다. React는 참조(reference)를 통해 값을 비교하므로, 새 값이 들어와야 무언가 변경되었다고 인식합니다.
URL 역시 단일 소유자(single owner)가 필요합니다. Backbone.history와 React Router 모두 URL을 감지하므로, 둘 중 하나를 선택해야 합니다. 초기에는 Backbone이 라우터를 담당하고 React는 작은 독립적인 영역(small islands)에 존재합니다. 대부분의 화면이 React로 전환되면 구조를 변경하세요: React Router가 주도권을 잡고, 남아있는 Backbone 화면들은 작은 React 래퍼 컴포넌트(wrapper components)에 의해 마운트됩니다. 그 이후부터는 라우트를 하나씩 이동시킬 수 있습니다.
Model과 Collection 대체하기
Backbone model은 한 가지처럼 보이지만, 실제로는 여러 작업을 동시에 수행합니다. 이 기능들을 분리하면 각각 간단한 React 대체재를 가질 수 있습니다:
| Backbone 모델의 역할 | React에서 좋은 선택지 |
|---|---|
url, fetch(), save() | TanStack Query를 통해 호출되는 일반 fetch 함수 |
| ... |
parse() 메서드는 겉보기보다 가치가 높습니다. 이들은 어떤 필드가 이름이 바뀌었는지 또는 어떤 엔드포인트가 결과를 감싸고 있는지 등 API에 대한 수년간의 지식을 담고 있습니다. 다른 작업을 하기 전에, 이들을 일반 함수로 옮기고 테스트를 작성하세요. 그래야 두 애플리케이션 모두 사용할 수 있습니다. 저희의 예시는 한 줄짜리였는데, 이는 상당히 전형적인 경우입니다:
// Backbone
const UserModel = Backbone.Model.extend({
urlRoot: 'https://api.github.com/legacy/user/search/',
...
React 버전에서 두 가지 작은 개선점을 발견할 수 있습니다: 사용자 이름을 URL에 인코딩하고, 아무것도 일치하지 않을 때 undefined 대신 빈 객체({})를 반환한다는 것입니다. 포팅은 이런 사소한 버그들을 고칠 좋은 기회입니다. 단, 하나씩 기록해두는 것이 중요합니다.
한 가지 더 염두에 두어야 할 점이 있습니다. Backbone 뷰(views)는 표시될 때마다 새로운 컬렉션(collection)을 생성하는 경우가 많았기 때문에, 클릭할 때마다 새로운 요청(request)을 보냈습니다. TanStack Query는 결과를 캐시(cache)하므로 일반적으로 속도 향상에 도움이 됩니다. 사용자가 버튼이 정말 새로고침되기를 기대하는 경우, 쿼리 키(query key)에 요청 ID를 추가하여 이전처럼 다시 가져오도록(fetch) 설정할 수 있습니다. 저희가 포팅 과정에서 그렇게 한 부분이 바로 그것입니다.
View에서 Component로
이것은 생각하는 방식에서 가장 큰 변화이므로, 실제 예시를 살펴보겠습니다. 여기는 사용자가 검색할 때 반응하는 저희의 Backbone 홈 뷰입니다. 이 코드는 DOM 요소를 찾고, 변경하며, jQuery 애니메이션을 실행합니다:
showAdditionalButtons: function() {
if (this.model.has('username')) {
this.avatar.attr('src', gravatarUrl(this.model.get('gravatar_id')));
...
React에서는 핸들러가 단순히 상태(state)를 업데이트합니다. 마크업(markup)은 각 상태가 어떻게 보이는지를 설명하며, 슬라이드는 클래스에 대한 CSS 전환(CSS transition)이 됩니다:
onSuccess: (user) => {
if (user.username) {
showAvatar(gravatarUrl(user.gravatar_id ?? ''));
...
팀원들이 이것에 익숙해지면, 한 그룹의 버그가 사라집니다. 페이지는 더 이상 데이터와 동기화되지 못하는 일이 없는데, 왜냐하면 페이지 자체가 항상 데이터를 기반으로 그려지기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

