왜 바닐라프론트를 만들었나!!

React vs VanillaFront — 유사점과 차이점

VanillaFront 2026. 9. 18. 23:27

 

한 줄 요약: 둘은 서로 다른 시장과 문제를 푸는 다른 방향의 접근입니다. 그런데 놀랍게도 ES6 클래스·모듈·async/await 같은 밑바닥이 완전히 같고, React 개발자가 VanillaFront에 자연스럽게 진입할 수 있는 유사점이 많습니다. 이 글은 React 개발자 관점에서 **이미 아는 것 90%, 새로 배울 것 10%**를 정직하게 짚습니다.


시작하며 — 대체가 아니라 서로 다른 방향

먼저 이 글의 톤을 명확히 합니다. VanillaFront는 React를 대체하려는 프레임워크가 아닙니다. React는 세계 최대 프론트엔드 라이브러리이자 압도적 생태계·커뮤니티·자료를 가진 훌륭한 도구입니다. 컨슈머 앱·랜딩 페이지·이커머스·글로벌 서비스 등 자유로운 UI 표현이 우선인 시장에서 React의 강점은 명확하고, 이 강점을 폄훼할 의도가 전혀 없습니다.

VanillaFront가 겨냥하는 자리는 다릅니다. **비즈니스 중심의 화면, 사내 업무 시스템(금융·공공·ERP·MES·WMS·기간계·엔터프라이즈 관리자 페이지·대시보드 등)**이 주 무대이고, 완성된 그리드·차트·트리·리포트가 처음부터 필요한 시장에 최적화되어 있습니다. 이 두 시장은 서로 배타적이지 않고 병존합니다.

이 글에서는 그럼에도 두 접근이 얼마나 많은 공통 언어를 공유하는지를 먼저 짚고 (React 개발자에게 이 사실이 반가운 소식일 것), 그다음에 어떤 지점에서 다른 설계 결정을 했는지를 정리합니다.


1. 놀랍도록 큰 공통점 — 밑바닥이 같다

많은 React 개발자가 처음엔 VanillaFront를 "다른 세계"로 인식합니다. 실제로는 사용하는 자바스크립트 문법·모듈 시스템·비동기 처리·이벤트 처리 방식이 대부분 동일합니다.

1.1 ES6 클래스와 모듈 — 완전 동일

둘 다 표준 ES6 클래스 선언 문법과 ES Modules를 그대로 사용합니다.

// React
import { Component } from 'react';
class UserList extends Component { /* ... */ }
export default UserList;

// VanillaFront
import { Va } from './lib/va.js';
class UserListView extends Va.View { /* ... */ }
export default UserListView;

두 프레임워크 모두 최신 자바스크립트 표준 위에 자연스럽게 얹혀 있습니다.

1.2 async/await, Promise, fetch — 완전 동일

// React 컴포넌트 안
useEffect(() => {
    fetch('/api/users')
        .then(r => r.json())
        .then(data => setUsers(data));
}, []);

// VanillaFront View 안
async mounted() {
    const res = await fetch('/api/users');
    const data = await res.json();
    this.getRef('userGrid').setData(data);
}

1.3 컴포넌트 기반 설계 — 개념 동일

두 프레임워크 모두 컴포넌트가 재사용 단위이고, props(속성)와 events(이벤트)로 통신합니다.

// React
<Button variant="primary" onClick={handleClick} disabled={loading}>
    저장
</Button>

// VanillaFront (config 트리 안)
{ tagName: 'button',
  variant: 'primary',
  text: '저장',
  disabled: this.loading,
  onClick: 'handleClick' }

1.4 라이프사이클 — 개념 동일, 이름만 다름

시점ReactVanillaFront

마운트 후 초기화 useEffect(() => {}, []) / componentDidMount mounted()
업데이트 반영 useEffect(() => {}, [dep]) / componentDidUpdate update()
언마운트 정리 return () => {} in useEffect / componentWillUnmount unmounted()

1.5 단방향 데이터 흐름 — 동일 원칙

둘 다 부모 → 자식은 속성으로 데이터 전달, 자식 → 부모는 이벤트로 통신하는 단방향 흐름을 따릅니다.

1.6 컴포넌트 확장 — ES6 extends 그대로

// React (클래스 컴포넌트 시절)
class SubmitButton extends Button { /* ... */ }

// VanillaFront
class SubmitButton extends Va.Button { /* ... */ }

1.7 Ref로 요소 참조 — 개념 동일, 이름만 다름

둘 다 ref라는 이름으로 요소 참조를 다룹니다.

// React (함수 컴포넌트 + useRef)
function LoginForm() {
    const nameInputRef = useRef(null);
    useEffect(() => {
        nameInputRef.current.focus();
    }, []);
    return <input ref={nameInputRef} />;
}

// VanillaFront
class LoginForm extends Va.View {
    mounted() {
        this.getRef('nameInput').focus();
    }
    config() {
        return { tagName: 'input', ref: 'nameInput' };
    }
}

개념 매핑: React의 ref={inputRef} ↔ VanillaFront의 ref: 'nameInput'. React의 inputRef.current ↔ VanillaFront의 this.getRef('nameInput').

1.8 이벤트 바인딩 — on<Event> 관습 동일

// React — 함수 참조 바인딩
function Form() {
    const handleClick = () => { /* ... */ };
    return <button onClick={handleClick}>저장</button>;
}

// VanillaFront — 메서드 이름 문자열 바인딩
class Form extends Va.View {
    handleClick() { /* ... */ }
    config() {
        return { tagName: 'button', text: '저장', onClick: 'handleClick' };
    }
}

커스텀 이벤트 정의도 개념 동일:

// React — props의 onXXX
<MyPicker onSelect={handleSelect} onCancel={handleCancel} />

// VanillaFront — events 배열 등록
Va.MyPicker = class extends Va.Component {
    constructor(option) {
        super('div', option);
        this.events = [...this.events, 'select', 'cancel'];
        this.setOption();
    }
    onUserPick(item) {
        this.dispatchEvent('select', this, item);
    }
}

// 사용 시
{ tagName: 'myPicker', onSelect: 'handleSelect', onCancel: 'handleCancel' }

1.9 스타일 정의 — style·className 관습 동일

// React
<div style={{ color: 'red', padding: '10px' }}
     className="user-panel">
    <input style={{ width: '200px' }} className="field-input" />
</div>

// VanillaFront
{ tagName: 'div',
  style: { color: 'red', padding: '10px' },
  cls: 'user-panel',
  tags: [
    { tagName: 'input',
      style: { width: '200px' },
      cls: 'field-input' }
  ]
}

개념 매핑: style={{...}} ↔ style: {...} (camelCase 완전 동일). className="..." ↔ cls: "..." (이름만 다름).

여기까지 정리하면: React 개발자의 스킬 중 대부분(ES6·모듈·async·fetch·컴포넌트 개념·라이프사이클 개념·상속·ref·이벤트 바인딩·스타일 관습)이 VanillaFront에 그대로 이관됩니다. 새 프레임워크 학습이 아니라, 알던 자바스크립트에 컴포넌트 카탈로그만 추가 학습하는 느낌에 가깝습니다.


2. 다른 지점 — 4가지 핵심 설계 결정

2.1 JSX vs JSON 데이터 트리

React — JSX + AG Grid

import { AgGridReact } from 'ag-grid-react';

function UserList({ users }) {
    return (
        <div className="user-list">
            <h1>사용자 목록</h1>
            <AgGridReact
                rowData={users}
                columnDefs={[
                    { field: 'id',    headerName: 'ID',    width: 80 },
                    { field: 'name',  headerName: '이름',  width: 120 },
                    { field: 'email', headerName: '이메일', width: 200 }
                ]}
            />
        </div>
    );
}

VanillaFront — JSON 데이터 트리 + 내장 Grid

class UserListView extends Va.View {
    mounted() {
        this.getRef('userGrid').setData(this.users);
    }
    config() {
        return {
            tagName: 'div',
            cls: 'user-list',
            tags: [
                { tagName: 'h1', text: '사용자 목록' },
                { tagName: 'grid',
                  ref: 'userGrid',
                  columns: [
                      { key: 'id',    title: 'ID',    width: 80 },
                      { key: 'name',  title: '이름',  width: 120 },
                      { key: 'email', title: '이메일', width: 200 }
                  ]
                }
            ]
        };
    }
}

대칭 정리:

  • 같은 컬럼 정의 (ID/이름/이메일, 폭 80/120/200)
  • 같은 데이터 바인딩
  • 차이는 표현의 껍데기: JSX 태그 트리 vs JSON 객체 트리
  • 차이는 그리드 출처: 별도 라이브러리 import vs 내장 (tagName: 'grid')
  • 속성 이름만 다름: field/headerName ↔ key/title

VanillaFront 팀은 왜 결국 JSON을 택했나 — 초기 JSX 검토와 오랜 숙고

VanillaFront 개발팀은 프레임워크 초기 설계 단계에서 JSX 방식을 심도 있게 검토했습니다. 그런데 검토 과정에서 몇 가지 사실을 다시 확인하게 됐고, 그 사실들이 결국 JSON을 선택하게 만들었습니다.

사실 1 — JSX는 웹 표준이 아닙니다.

많은 사람이 오해하는 지점입니다. JSX는 자바스크립트 표준(ECMAScript)에 포함되지 않았고, 브라우저가 네이티브로 실행하지 못합니다. JSX 코드는 반드시 Babel 등의 트랜스파일러로 변환되어야 실행 가능한 상태가 됩니다:

// 개발자가 작성하는 JSX
<div className="user">
    <span>{user.name}</span>
</div>

// Babel이 변환한 실제 실행 코드
React.createElement('div', { className: 'user' },
    React.createElement('span', null, user.name)
);

즉 JSX를 사용한다는 것은 빌드 파이프라인·트랜스파일러·소스맵을 아키텍처에 포함시킨다는 결정이었고, 이 결정은 VanillaFront의 No-Build 방향과 근본적으로 상충했습니다.

사실 2 — JSX든 JSON이든 결국 "변환"을 거칩니다.

두 접근 모두 "선언된 구조 → 실제 DOM"이라는 변환을 거친다는 같은 원리입니다:

JSX 접근:  JSX 문법 → Babel 트랜스파일 → React.createElement 호출 → Virtual DOM → 실제 DOM
JSON 접근: JS 객체 리터럴 → (변환 단계 없음) → 프레임워크가 트리 순회 → 실제 DOM

사실 3 — 자바스크립트 엔진은 JSON·객체 리터럴에 최적화되어 있습니다.

이 부분이 결정적이었습니다.

  • JSON 파싱은 자바스크립트에서 가장 최적화된 연산 중 하나입니다. JSON.parse는 네이티브 C++ 구현으로 수십 년간 다듬어져 왔고, V8·SpiderMonkey·JavaScriptCore 모두 극한 최적화 대상.
  • 객체 리터럴 {...} 평가는 자바스크립트 언어의 가장 기본 연산입니다. 별도 파싱·트랜스파일·변환 없이 엔진이 즉시 실행. 오버헤드 0.
  • 반면 JSX는 자바스크립트 엔진이 이해 못 하는 문법이라 반드시 사전 변환이 필요하고, 변환된 결과는 createElement 함수 호출 트리 — 이 함수 호출도 각각 오버헤드가 있음.

사실 4 — 도구 생태계 관점의 부가 이점

  • JSON 표준 도구가 VanillaFront config를 그대로 다룰 수 있음
  • AI 도구가 데이터 트리를 native로 다룸
  • IDE 시각 편집기가 객체 리터럴 파싱·수정이 JSX보다 훨씬 결정적

결론: JSX와 JSON은 "구조 선언 → DOM 변환"이라는 같은 원리이지만, 자바스크립트 엔진이 어느 쪽에 더 최적화되어 있는지와 트랜스파일 파이프라인이 필요한지에서 실질 차이가 납니다. VanillaFront는 오랜 검토 끝에 웹 표준 위에서 엔진 최적화를 그대로 활용하고 빌드 파이프라인 없이 실행되는 JSON 데이터 트리를 택했습니다.

2.2 Hooks vs 명시적 인스턴스 상태 — 상태 관리의 다른 철학

이 부분이 두 프레임워크의 가장 큰 철학적 차이입니다.

React — Hooks + 함수 컴포넌트

function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const filtered = useMemo(() => users.filter(u => u.active), [users]);

    useEffect(() => {
        setLoading(true);
        fetch('/api/users')
            .then(r => r.json())
            .then(data => {
                setUsers(data);
                setLoading(false);
            });
    }, []);
}

React 방식의 특징:

  • 함수 컴포넌트는 렌더마다 재실행됨
  • 상태는 useState로 hook 외부에 유지
  • 사이드 이펙트는 useEffect
  • 훅 규칙: 최상단, 조건문 안 사용 금지, 의존성 배열 주의
  • StrictMode 이중 실행 대비 — 놓치기 쉽지만 결정적

⚠ React.StrictMode 이중 실행 — 놓치기 쉽지만 결정적인 지점

React 개발자 사이에서도 정확히 이해하지 못한 채 넘어가는 경우가 많은 지점입니다. React 18부터 <StrictMode>로 감싼 컴포넌트는 개발 모드에서 의도적으로 두 번 실행됩니다:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

createRoot(document.getElementById('root')).render(
    <StrictMode>
        <App />
    </StrictMode>
);

무엇이 두 번 실행되나:

  • 함수 컴포넌트 본문 자체
  • useState·useReducer의 initializer 함수
  • useMemo·useCallback의 팩토리 함수
  • useEffect·useLayoutEffect의 setup + cleanup 사이클
  • 클래스 컴포넌트의 constructor·render

왜 이 지점이 중요한가 — 모르고 무시하면 문제가 커집니다:

  • StrictMode 이중 실행을 "귀찮은 개발 모드 버그"로 오해하고 StrictMode를 끄고 개발하는 팀이 실제로 있음 — 프로덕션 Concurrent Rendering 상황에서 그대로 터짐
  • useEffect에서 fetch·타이머·구독을 정리 없이 시작하면 이중 실행 시 리소스 누수 노출 → 프로덕션에서 조용히 누적
  • 렌더 함수 안에서 객체 뮤테이션·전역 카운터 증가 같은 코드가 있으면 값이 두 배 됨
  • React 팀은 StrictMode를 기본으로 사용할 것을 강력 권고하지만, 개발자 다수가 이 신호를 정확히 해석 못 하고 넘어감

VanillaFront — 명시적 인스턴스 상태

class UserListView extends Va.View {
    async mounted() {
        this.loading = true;
        const res = await fetch('/api/users');
        this.users = await res.json();
        this.loading = false;
        this.getRef('userGrid').setData(this.users);
    }
}

VanillaFront 방식의 특징:

  • 클래스 인스턴스가 수명 내 유지됨 (재실행 안 함)
  • 상태 = this.xxx — 그냥 인스턴스 프로퍼티
  • 훅 순서 규칙·의존성 배열 없음
  • 이중 실행 개념 자체가 없음

2.3 렌더링 모델 — Virtual DOM vs 직접 DOM 조작

과거에는 DOM에 대한 직접조작이 속도를 느리게하는 원인이었습니다. 따라서 Virtual DOM 구현은 큰 장점이었습니다. 지금은 JVM 속도가 개선됨에 따라 이에 대한 잇점이 크게 없습니다. 더우기 경우에 따라서는 가상돔이 더 느리기도 합니다.

 

React: 상태 변경 → 함수 재실행 → Virtual DOM 생성 → diff → 실제 DOM 패치. 개발자는 선언적으로 상태만 관리.

VanillaFront: 상태 변경 → this.getRef('userGrid').setData(data) 같은 명시적 DOM 반영. Virtual DOM 오버헤드 없음. 대규모 그리드·실시간 대시보드에 유리.

2.4 조립형 vs 완결형 — 엔터프라이즈 컴포넌트

React (조립형): 기반 라이브러리만. 그리드·차트·다이어그램은 별도 상용 라이브러리. 자유도 큰 대신 라이선스·통합·버전 관리 분산.

VanillaFront (완결형): 기반 + 엔터프라이즈 컴포넌트가 같은 라이선스. 통합 학습·유지보수·라이선스 단일화.


3. 로컬·컴포넌트 상태 — 인스턴스 프로퍼티와 DataManager

Redux·Zustand·Recoil 같은 별도 상태 관리 라이브러리 없이 표준 자바스크립트 관습만으로 대부분 시나리오가 해결됩니다.

3.1 로컬 상태 = 인스턴스 프로퍼티

class CounterView extends Va.View {
    mounted() {
        this.count = 0;
        this.updateDisplay();
    }

    onIncrement() {
        this.count++;
        this.updateDisplay();
    }

    updateDisplay() {
        this.getRef('countLabel').setText(String(this.count));
    }

    config() {
        return {
            tagName: 'div',
            tags: [
                { tagName: 'label', ref: 'countLabel', text: '0' },
                { tagName: 'button', text: '+1', onClick: 'onIncrement' }
            ]
        };
    }
}

훅 없이 this.count가 곧 상태입니다.

3.2 컴포넌트 상태 = DataManager

여러 필드가 있는 데이터 셋 상태는 Va.DataManager로 관리:

const dm = new Va.DataManager();

// 데이터 로드
dm.setData([
    { id: 1, name: '홍길동', active: true },
    { id: 2, name: '김철수', active: false }
]);

// 변경 이벤트 자동 발생
dm.addEventListener('change', (sender, action, items) => {
    // action: 'set', 'add', 'modify', 'remove' 등
});

// 그리드에 바인딩
this.getRef('userGrid').setDataManager(dm);

// 어디서든 조작
dm.addRow({ id: 3, name: '이영희', active: true });
dm.modifyRow(0, { active: false });
dm.rollback();  // 롤백도 표준

Redux의 액션·리듀서·스토어 개념을 표준 EventTarget API로 구현한 형태. 학습 부담이 극단적으로 낮습니다. 


4. 지속 상태 — 새로고침·뒤로가기에도 살아남는 상태

실무에서 자주 간과되지만 결정적으로 중요한 지점입니다.

4.1 시나리오 — 이 문제가 왜 자주 발생하는가

"사용자가 폼에 값을 입력 → 다른 화면으로 이동 → 브라우저 뒤로가기 → 값이 그대로 복원되어야 한다. 또는 F5 새로고침 후에도 화면 상태가 유지되어야 한다."

엔터프라이즈 관리자 페이지·기간계에서 실수로 새로고침은 매일 일어나고, 다단계 워크플로우에서 뒤로가기가 자연스러워야 하는 요건은 표준적입니다.

4.2 React에서 이 시나리오 처리

React 자체에는 지속 상태 API가 없습니다. 개발자가 별도 접근으로 해결:

  • redux-persist 미들웨어
  • zustand + persist 미들웨어
  • recoil-persist
  • localStorage·sessionStorage 수동 관리
  • URL query params + React Router location.state

각각 학습·설정·정리 흐름이 필요하고, 어느 방식을 선택할지 팀 결정이 반복됩니다.

4.3 VanillaFront — History API 기반 내장

브라우저 표준 History API(history.state)를 활용한 지속 상태 API를 프레임워크에 내장했습니다:

class UserFormView extends Va.View {
    mounted() {
        // 저장된 상태 복원 (새로고침·뒤로가기 후)
        const state = this.getState();
        if (state) {
            this.getRef('name').setValue(state.name || '');
            this.getRef('age').setValue(state.age || '');
        }
    }

    onNameChange() {
        // 값 변경 시 상태 저장 — history.state에 반영
        this.setState('name', this.getRef('name').getValue());
    }

    onAgeChange() {
        this.setState('age', this.getRef('age').getValue());
    }

    // 개별 값 접근은 키를 넘기면 됨
    getSavedName() {
        return this.getState('name');
    }

    config() {
        return {
            tagName: 'article',
            tags: [
                { tagName: 'inputField', ref: 'name',
                  label: '이름', onChange: 'onNameChange' },
                { tagName: 'inputField', ref: 'age',
                  label: '나이', onChange: 'onAgeChange' }
            ]
        };
    }
}

4.4 자동으로 지원되는 시나리오

  • 페이지 새로고침(F5) — 같은 URL 로드 시 history.state 그대로 유지
  • 브라우저 뒤로가기·앞으로가기 — 각 히스토리 스택마다 자체 상태 보관
  • 라우터 이동 후 복귀 — 다른 View로 이동했다가 돌아와도 상태 유지
  • 탭 유지 시 세션 지속 — 브라우저 탭이 열려 있는 동안 유지

4.5 핵심 2개 메서드

메서드역할

this.setState(key, value) 특정 키에 값 저장 (history.state에 병합)
this.getState([key]) 인자 없으면 전체 상태, 키 넘기면 해당 값만 반환

4.6 왜 이 지점이 실무에서 중요한가

  • 엔터프라이즈 관리자 페이지·기간계에서 실수로 새로고침은 매일 일어남
  • 여러 화면을 왕복하는 다단계 워크플로우에서 뒤로가기가 자연스러워야 함
  • React 프로젝트에서 이걸 위해 persist 라이브러리 도입·설정·정리가 반복 학습 대상
  • VanillaFront는 이 요건을 프레임워크의 표준 기능으로 흡수
  • VanillaFront는 보통 업무중심의 복잡한 화면이기 때문에 화면별 멀티탭을 두고 여러개 를 보여주는 형태일 경우 state는 사용하지 않을 수도 있습니다.

정리하면: VanillaFront의 지속 상태 API는 브라우저 표준 History API 위에 자연스럽게 얹힌 2개 메서드일 뿐이지만, 이 표준 활용이 엔터프라이즈 시나리오에서 지속 상태 관리 라이브러리를 별도 도입할 필요를 없앱니다.


5. 글로벌 이벤트 — 구독한 View는 어디서든 수신

Context API·Redux·Zustand 없이도 표준 이벤트 패턴으로 앱 전역 통신이 가능합니다.

5.1 pub-sub 모델 — 자동 전파가 아닌 구독 기반

동작 방식은 표준 pub-sub입니다. 프레임워크가 유지하는 중앙 이벤트 허브(start view 요소)에 등록된 리스너에게 이벤트가 전달됩니다. 모든 View에 자동 나가는 게 아니라, Va.addCommonEventListener로 구독한 View만 수신.

5.2 코드 예시

// 어디서든 이벤트 발생
Va.dispatchCommonEvent('user:login', { userId: 42, name: '홍길동' });
Va.dispatchCommonEvent('cart:updated', cartItems);
Va.dispatchCommonEvent('theme:changed', 'dark');

// 원하는 View에서 각자 구독 (mounted 등에서)
class HeaderView extends Va.View {
    mounted() {
        Va.addCommonEventListener('user:login', (user) => {
            this.getRef('nameLabel').setText(user.name);
        });
    }
}

class CartView extends Va.View {
    mounted() {
        Va.addCommonEventListener('cart:updated', (items) => {
            this.getRef('cartBadge').setText(String(items.length));
        });
    }
}

5.3 글로벌 이벤트의 이점

  • 별도 라이브러리·설정·프로바이더 불필요
  • 내부적으로 표준 CustomEvent·EventTarget 확장이라 이미 익숙한 패턴
  • 컴포넌트 간 결합도 낮음 — 발생자와 수신자가 서로 몰라도 됨 (pub-sub)
  • 디버깅 쉬움 — 이벤트 로그로 흐름 추적 가능
  • Vue의 mitt, React의 eventemitter 같은 도구를 프레임워크가 통합 제공

5.4 동작 원리

프레임워크 초기화 시 지정된 start view 요소가 중앙 이벤트 허브 역할을 합니다. Va.dispatchCommonEvent는 그 허브에 CustomEvent를 dispatch하고, Va.addCommonEventListener는 그 허브에 리스너를 등록합니다.


6. 상태 관리 5계층 요약

범위React의 일반 관습VanillaFront의 표준 관습

로컬 상태 (컴포넌트 안) useState this.xxx 인스턴스 프로퍼티
파생 상태 (계산) useMemo 표준 getter
데이터 셋 (리스트·그리드) useState + 배열 관리 Va.DataManager
지속 상태 (새로고침·뒤로가기) redux-persist / zustand persist / localStorage 수동 this.setState / this.getState (History API 내장)
글로벌 상태 (앱 전역) Context / Redux / Zustand 글로벌 이벤트 + 싱글턴 서비스

VanillaFront에서는 5계층 모두 표준 자바스크립트 패턴·표준 브라우저 API만으로 해결됩니다.

React 개발자에게 특히 매력적일 지점: "상태 관리 라이브러리 학습을 안 해도 되는 프레임워크"입니다. Redux vs Zustand vs Jotai vs Recoil 결정 고민이 없고, Context API의 리렌더 최적화 고민도 없습니다.


7. 코드 대비 — 같은 화면, 두 스택

7.1 React (조립형 + JSX + Hooks)

import { useState, useEffect } from 'react';
import { AgGridReact } from 'ag-grid-react';

function UserListView() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetch('/api/users')
            .then(r => r.json())
            .then(data => {
                setUsers(data);
                setLoading(false);
            });
    }, []);

    const handleReset = () => setUsers([]);

    if (loading) return <div>Loading...</div>;

    return (
        <div className="panel">
            <h1>사용자 목록</h1>
            <AgGridReact
                rowData={users}
                columnDefs={[
                    { field: 'id',    headerName: 'ID',    width: 80 },
                    { field: 'name',  headerName: '이름',  width: 120 },
                    { field: 'email', headerName: '이메일', width: 200 }
                ]}
            />
            <button onClick={handleReset}>초기화</button>
        </div>
    );
}

7.2 VanillaFront (완결형 + JSON 트리 + 인스턴스 상태)

export default class UserListView extends Va.View {
    async mounted() {
        const res = await fetch('/api/users');
        const users = await res.json();
        this.getRef('userGrid').setData(users);
    }

    onReset() {
        this.getRef('userGrid').setData([]);
    }

    config() {
        return {
            tagName: 'page',
            tags: [{
                tagName: 'panel',
                title: '사용자 목록',
                tags: [
                    { tagName: 'grid',
                      ref: 'userGrid',
                      columns: [
                          { key: 'id',    title: 'ID',    width: 80 },
                          { key: 'name',  title: '이름',  width: 120 },
                          { key: 'email', title: '이메일', width: 200 }
                      ]
                    },
                    { tagName: 'button', text: '초기화', onClick: 'onReset' }
                ]
            }]
        };
    }
}
Va.registerView('/view/user/UserList', UserListView);

7.3 두 코드에서 같은 것과 다른 것

완전 동일한 것

  • ES6 클래스·import·export default
  • async/await·fetch API
  • 표준 자바스크립트 문법 전반

다른 것

  • 화면 정의: JSX(React) vs JSON 트리(VanillaFront)
  • 상태 관리: useState(React) vs 인스턴스 프로퍼티(VanillaFront)
  • 그리드: 별도 라이브러리(React) vs 내장(VanillaFront)

8. React 개발자의 스킬 이관 지도

8.1 그대로 활용 가능 (90%)

  • ES6 문법 전반 (class, extends, arrow function, spread, destructuring)
  • ES Modules (import, export, default)
  • async/await, Promise
  • fetch API, 표준 DOM API
  • 이벤트 처리 관습
  • 컴포넌트 재사용 개념
  • 라이프사이클 개념
  • 비동기 데이터 흐름

8.2 새로 배울 것 (10%)

  • Va.View, Va.Component 베이스 클래스의 구조
  • config() 데이터 트리 문법 (JSX와 다른 형태)
  • Va.registerView·Va.registerComponent 등록 관습
  • 컴포넌트 카탈로그 (tagName들 — grid, tree, chart, dialog 등)
  • 글로벌 이벤트 패턴 (표준 EventTarget 확장)
  • DataManager 사용법

8.3 예상 적응 기간

React 경력 개발자가 VanillaFront에 적응하는 시간은 일반적으로 1~3일 수준으로 예상됩니다.


9. 언제 각 스택이 자연스러운가

React가 자연스러운 조직

  • 컨슈머 앱·랜딩 페이지·이커머스·SaaS — 자유로운 UI 표현이 우선
  • 거대한 커뮤니티·자료·라이브러리 생태계 활용이 결정적
  • SSR/SSG(Next.js 등)가 필수 요건
  • React 개발자 인력 풀 활용이 조직 전략
  • 기존 React 자산이 이미 크게 축적

VanillaFront가 자연스러운 조직

  • 비즈니스 중심의 화면, 사내 업무 시스템 (금융·공공·ERP·MES·WMS·기간계·관리자 페이지·대시보드 등)
  • 완성된 그리드·차트·트리·리포트가 처음부터 필요
  • 대형 프로젝트에서 라이선스 통합·법무 부담 감소가 우선
  • 사내 인트라넷·폐쇄망 등 No-Build 배포가 유리한 환경
  • AI 도구·표준 IDE 통합이 결정적
  • 레거시 시스템에 점진 도입이 필요 (Shadow DOM 격리)

10. 마무리

React는 훌륭한 범용 UI 라이브러리이고, 세계 최대 프론트엔드 생태계의 중심입니다.

VanillaFront는 다른 시장을 겨냥한 다른 접근입니다. 엔터프라이즈 완결형·표준 스택·AI 친화·Shadow DOM 격리 등이 우선순위인 조직에게 자연스러운 선택.

이 글의 핵심 메시지:

  • 두 프레임워크는 ES6·모듈·async/await·컴포넌트 개념·라이프사이클 개념·상속·ref·이벤트 바인딩·스타일 관습 등 밑바닥이 대부분 같음
  • 다른 것은 JSX vs JSON 트리, Hooks vs 인스턴스 상태, 조립형 vs 완결형 등 4가지 설계 결정
  • React 개발자가 VanillaFront에 1~3일이면 자연스럽게 적응 가능
  • VanillaFront는 상태 관리 라이브러리 학습 없이도 인스턴스 상태·DataManager·글로벌 이벤트·History API 기반 지속 상태로 대부분 시나리오 해결

React 개발자에게 이 글의 결론: VanillaFront는 낯선 세계가 아닙니다. 이미 아는 자바스크립트 90%에 컴포넌트 카탈로그 10%만 얹으면 되는 프레임워크이고, React의 강점을 대체하려는 게 아니라 엔터프라이즈 시장의 다른 요건에 답하는 다른 방향의 도구입니다. 조직의 우선순위와 요건에 맞는 스택을 선택하시면 됩니다.


VanillaFront 관련 링크