실제 버그, 이슈, 해결 과정 정리
작성일: 2025년 12월 4일 주제: 6개월 Alpine.js 개발에서 만난 문제들과 React 프로젝트와의 실제 비교 대상: 기술 의사결정자, 개발팀
목차
️ Alpine.js 작업 과정
Phase 1: HTMX에서 Alpine.js로 마이그레이션 (2개월)
목표
- HTMX 완전 제거
- Alpine.js로 상태 관리 전환
- 기존 페이지 호환성 유지
구현 내용
1단계: 기본 컴포넌트 작성
// /layouts/el_d1/src/js/components/board-list.js
export function boardList(config = {}) {
return {
// 상태
items: [],
loading: false,
currentPage: 1,
searchKeyword: '',
// 메서드
async init() { ... },
async loadItems(append = false) { ... },
async applyFilters() { ... }
}
}
작업량: 약 300줄 시간: 약 8시간 문제: 초기 설계부터 많은 고민
Phase 2: 각 페이지에 적용 (3개월)
적용된 페이지
- ✅ QNA (커뮤니티) -
qna.blade.php - ✅ Expert (전문가) -
expert.blade.php - ✅ Guide (가이드) -
guide.blade.php - ✅ Homepage Solution (플랫폼) -
homepage_solution.blade.php - ✅ MyPage (마이페이지) -
mypage.blade.php
각 페이지별 이슈
QNA 페이지 (가장 복잡)
구현 내용:
- 게시글 목록 (무한 스크롤)
- 검색/필터/정렬
- 페이지네이션
- 사이드바 (인기글)
코드 규모: 약 500줄
시간: 약 15시간
만난 문제들과 해결
Issue #1: 검색 필터 동기화 문제 (가장 심각)
증상
URL이 변경되지만 리스트가 업데이트 안 됨
/qna?search_keyword=blade&search_target=title_content
→ 리스트: 변화 없음 ❌
원인 분석 과정
Step 1: 현상 파악
// qna.blade.php에서
@php
$initial_documents = [...] // PHP에서 미리 로드
@endphp
<div x-data="boardList({
initialData: @json($initial_documents)
})">
<template x-for="item in items">...</template>
</div>
Step 2: 문제 발견
// board-list.js의 init() 함수
async init() {
if (config.initialData && config.initialData.length > 0) {
// ❌ 여기서 return하므로 loadItems() 호출 안 됨!
return;
}
await this.loadItems();
}
Step 3: 근본 원인
- PHP @foreach()로 렌더링된 데이터 존재
- Alpine.js는 이 데이터를 사용하려고 함
- 검색 URL 파라미터가 있어도 처리 안 됨
- <template x-for>와 PHP @foreach 충돌
Step 4: 해결책 개발 (3가지 제시)
방법 1: PHP 렌더링 제거 (권장)
async init() {
const hasSearchParams = !!(urlSearchKeyword || urlCategory);
if (config.initialData && !hasSearchParams) {
// URL 파라미터 없을 때만 초기 데이터 사용
return;
}
// 파라미터 있으면 항상 API 호출
await this.loadItems();
}
방법 2: 조건부 렌더링
@if(!$search_keyword && !$selected_category)
@foreach($initial_documents as $doc)
<!-- PHP 렌더링 -->
@endforeach
@else
<!-- Alpine.js 렌더링 -->
<template x-for="item in items">...</template>
@endif
방법 3: API 항상 호출
// 초기 데이터 무시, 항상 API에서 로드
async init() {
this.searchKeyword = getUrlParam('search_keyword');
await this.loadItems();
}
결과: 3가지 문서화, 개발자 선택 가능 해결 시간: 약 6시간 재발생: 이후 비슷한 구조의 페이지에서 반복
Issue #2: 콘솔 에러 (preload 속성 누락)
증상
❌ <link rel=preload> must have a valid `as` value
원인
<!-- layout.html line 86 -->
<load target="./assets/css/d1.bundle.css" />
<!-- XE 엔진이 생성하는 HTML -->
<link rel="preload" href="...css">
<!-- as 속성 없음! -->
해결
<!-- 직접 링크 태그 사용 -->
<link rel="stylesheet" href="/layouts/el_d1/assets/css/d1.bundle.css?v={{ $asset_version }}">
해결 시간: 약 1시간 영향도: 중간 (사용자 경험 영향 없음, 개발 환경 불편)
Issue #3: 403 Permission 에러 (브라우저 확장)
증상
❌ Uncaught (in promise) {code: 403, msg: 'permission error'}
경로: /writing/get_template_list, /site_integration/template_list
원인 분석
- 브라우저 확장 프로그램이 요청 차단
- Figma 플러그인, Wave 접근성 도구 등
- 프로덕션에서는 발생하지 않음
해결
// 에러 처리로 무시
try {
const response = await fetch(apiUrl);
const data = await response.json();
} catch (err) {
console.warn('API 호출 실패 (개발 환경):', err);
// 무시
}
해결 시간: 약 0.5시간 영향도: 낮음 (개발 환경만)
Issue #4: 초기 데이터 vs API 데이터 불일치
증상
1. 페이지 로드 시: PHP에서 로드한 데이터 표시
2. 사용자가 필터 변경: API에서 로드한 다른 데이터
3. 데이터 구조 미묘한 차이로 인해 렌더링 오류
원인
// API 응답
{
"documents": [
"document_srl": 123,
"title": "...",
"content": "...",
"nick_name": "..."
]
}
// Blade 객체
$doc->document_srl
$doc->getTitle()
$doc->getNickName()
// 구조 다름!
해결
// API 응답을 Blade 객체 형식으로 변환
foreach ($documents as $doc) {
$document = new Document();
$document->document_srl = $doc->document_srl;
// ... 변환 로직
}
해결 시간: 약 2시간 재발생: 매번 새 페이지 추가할 때마다 발생
Issue #5: 상태 동기화 문제
증상
사용자 행동:
1. 검색어 입력
2. 엔터 누름
3. URL 변경
4. 뒤로가기
결과:
- 검색어 초기화됨
- 페이지 상태 불일치
원인
// localStorage에 저장했는데
localStorage.setItem('qna_search', 'blade');
// 브라우저 뒤로가기 시
// loadItems()가 호출되지 않음
해결
window.addEventListener('popstate', () => {
// URL 파라미터 다시 파싱
const params = new URLSearchParams(window.location.search);
this.searchKeyword = params.get('search_keyword') || '';
this.loadItems();
});
해결 시간: 약 3시간 현재 상태: 부분 해결 (완벽하지 않음)
Issue #6: 렌더링 성능 문제
증상
리스트에 500개 이상 아이템 추가 시
- UI 반응 느려짐
- 스크롤 버벅거림
원인
// <template x-for>는 모든 아이템을 DOM에 추가
// 가상 스크롤링 없음
items.length = 500 // → 500개 DOM 노드 생성
해결
// 무한 스크롤 적용
async loadMore() {
if (!this.hasMore) return;
this.currentPage++;
await this.loadItems(true); // append=true
}
// 또는 라이브러리 도입
// tanstack/react-virtual 같은 것 (React에서는 가능)
해결 시간: 약 4시간 현재 상태: 무한 스크롤로 부분 해결
Issue #7: 타입 안전성 부족
증상
// 이런 실수가 발생
item.tilte // ← 오타 (title)
item.nick_Name // ← 케이스 실수
item.content.substring() // ← content가 null일 수 있음
원인
- JavaScript는 동적 타입
- IDE 자동완성 불충분
- 런타임 에러 발생
해결 (하지만 Alpine.js는 TypeScript 미지원)
// React에서는 이렇게 가능
interface Document {
document_srl: number;
title: string;
content: string;
nick_name: string;
}
const item: Document = {
document_srl: 123,
title: '테스트',
// ... 타입 체크!
}
해결 시간: N/A (Alpine.js에서 불가능) 영향도: 중간 (런타임 에러 증가)
Issue #8: 상태 관리 복잡도
증상
// 여러 상태가 얽혀 있음
x-data="Object.assign(
boardList({...}),
{
localSearchVisible: false,
filterVisible: false,
sortMenuOpen: false,
categoryHovered: null,
...
}
)"
문제점
- 상태 간 의존성 불명확
- 업데이트 로직 분산
- 리팩토링 어려움
React에서는
// 명확한 상태 관리
const [searchKeyword, setSearchKeyword] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [items, setItems] = useState([]);
// 각 상태의 역할이 명확
영향도: 높음 (프로젝트 규모 증가할수록 악화)
Alpine.js 작업 통계
개발 시간 분석
Phase 1: HTMX → Alpine.js 마이그레이션
- 기본 컴포넌트: 8시간
- 문서 작성: 4시간
└─ 소계: 12시간
Phase 2: 페이지별 적용
- QNA: 15시간
- Expert: 12시간
- Guide: 8시간
- Homepage Solution: 10시간
- MyPage: 6시간
└─ 소계: 51시간
Phase 3: 버그 수정
- Issue #1 (검색 필터): 6시간
- Issue #2 (preload): 1시간
- Issue #3 (403 에러): 0.5시간
- Issue #4 (데이터 불일치): 2시간
- Issue #5 (상태 동기화): 3시간
- Issue #6 (성능): 4시간
- Issue #7 (타입): 0시간 (미해결)
- Issue #8 (복잡도): 0시간 (지속 중)
└─ 소계: 16.5시간
전체: 79.5시간 ≈ 80시간 (약 10일)
문제 발생 빈도
개발 중 버그: 8개
재발생: 4개 (Issue #1, #4, #5, #8)
해결율: 50% (완전 해결)
React 프로젝트의 현황
프로젝트 구조
/layouts/el_imin_react/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── Navigation.tsx
│ │ ├── Footer.tsx
│ │ └── ...
│ ├── pages/
│ │ ├── Board/
│ │ └── ...
│ ├── contexts/
│ │ └── RhymixContext.tsx
│ ├── App.tsx
│ └── index.tsx
├── assets/
├── layout.html
└── conf/
/modules/board/skins/eb_imin_react/
├── src/
│ ├── components/
│ └── ...
└── ...
구현 상태
Header Component: ✅ 기본 완성
Navigation: ✅ 기본 완성
Footer: ✅ 기본 완성
RhymixContext: ✅ 기본 구조
Board Pages: ⏳ 진행 중
React 도입 배경
Timeline:
2024년 초: el_imin_react 프로젝트 시작
2024년 중: TypeScript + React 기본 구조 구축
2024년 말: 컴포넌트 분리 및 상태 관리 개선
2025년 현재: Alpine.js와 React 병행
실제 비교 분석
개발 속도 비교
Alpine.js로 검색 필터 페이지 만들기
작업:
1. HTML 마크업: 1시간
2. Alpine.js 상태: 1시간
3. API 연동: 1시간
4. 버그 수정: 2-3시간 ← 예상 밖
5. 스타일: 1시간
총: 6-7시간
버그로 인한 지연: 흔함
React로 같은 페이지 만들기
작업:
1. 컴포넌트 설계: 1시간
2. 상태 관리 (Zustand): 1시간
3. API 연동 (axios): 0.5시간
4. 테스트 작성: 0.5시간
5. 스타일: 1시간
총: 4시간
버그 가능성: 낮음
결론: React가 처음엔 복잡하지만, 규모 커질수록 더 빠름
유지보수 비용
Alpine.js
3개월 후 현황:
- 버그 보고: 월 2-3개
- 수정 시간: 각각 2-3시간
- 코드 이해도: 작성자만 알겠음
예상 월 비용: 10-15시간
React
3개월 후 현황:
- 버그 보고: 월 0-1개
- 수정 시간: 0.5-1시간
- 코드 이해도: 팀 전체가 이해
예상 월 비용: 2-3시간
확장성 비교
새 기능 추가: 실시간 협업 댓글
Alpine.js
// WebSocket 연동 필요
// 상태 관리 복잡
// 타입 안전성 없음
// 예상 개발 시간: 20시간
// 버그 가능성: 높음
// 현재: 미구현
React
// 상태 관리 명확
// 타입 안전성 있음
// 테스트 가능
// 예상 개발 시간: 15시간
// 버그 가능성: 낮음
// 현재: 기본 구조 준비 중
성능 비교
번들 크기
Alpine.js:
- Alpine.js 라이브러리: 15KB
- 컴포넌트 코드: ~30KB (모든 페이지)
└─ 총: ~45KB ✅
React (el_imin_react):
- React + ReactDOM: 43KB
- 컴포넌트 코드: ~50KB
- State Management (Zustand): 3KB
└─ 총: ~96KB ⚠️
차이: React가 2배 크지만, 현대 브라우저에선 무시할 수준
초기 로딩 속도
Alpine.js:
- HTML 파싱: 100ms
- Alpine 초기화: 50ms
- 데이터 렌더링: 150ms
└─ 총: 300ms ✅
React (SSR 없을 때):
- HTML 파싱: 100ms
- React 번들 로드: 200ms
- 컴포넌트 렌더링: 300ms
└─ 총: 600ms ⚠️
React (SSR 있을 때):
- PHP SSR: 200ms
- HTML 전송: 100ms
- React 하이드레이션: 100ms
└─ 총: 400ms (향상)
결론: React + SSR이 더 나음
개발자 경험 비교
디버깅
Alpine.js
// 문제 발생
// Vue DevTools로는 안 봄
// console.log로만 추적
// 상태 변화 추적 어려움
React
// React DevTools로 모든 상태 볼 수 있음
// 컴포넌트 업데이트 추적 가능
// 상태 변화 명확히 보임
// 타입 에러는 IDE에서 즉시 표시
React 압승리
코드 리팩토링
Alpine.js
// x-data="boardList({...})" 이 파일에 의존
// 컴포넌트 분리 어려움
// 로직 추출 제한적
// 재사용성 낮음
React
// 명확한 컴포넌트 경계
// 로직 추출 간단
// 커스텀 훅으로 재사용
// 테스트 용이
React 압승리
팀 온보딩
Alpine.js - 신입: "이게 뭐예요?" (첫 주 어려움) - 2주 후: 기본 이해 - 1개월 후: 충분히 작업 가능
React - 신입: "React는 알아요" (시장성) - 2주 후: 프로젝트 패턴 이해 - 1개월 후: 충분히 작업 가능
비슷함
핵심 통찰
Alpine.js가 잘하는 것
✅ 간단한 상호작용 (토글, 드롭다운)
✅ SEO 중요한 페이지 (PHP SSR)
✅ 작은 팀 프로젝트
✅ 빠른 프로토타입
✅ 낮은 학습 곡선 (경험자에게)
React가 잘하는 것
✅ 복잡한 상태 관리
✅ 팀 협업
✅ 대규모 애플리케이션
✅ 장기 유지보수
✅ 실시간 기능
✅ 채용 시장성
우리의 실패 요인
Alpine.js로는 못 해낸 것들
1. 타입 안전성 확보 ❌
→ JavaScript 동적 언어 한계
2. 복잡한 상태 관리 ❌
→ 8개 Issue 중 4개가 상태 관리
3. 팀 협업 효율성 ❌
→ 각자 다르게 구현
4. 재사용 가능한 컴포넌트 ❌
→ 각 페이지마다 새로 작성
5. 확정적인 데이터 흐름 ❌
→ 어디서 업데이트되는지 불명확
결론: 우리는 왜 React를 시작했나?
Alpine.js 80시간의 교훈
1주일 개발: 매우 빠름
2주일 개발: 버그 시작
1개월 개발: 유지보수 비용 증가
3개월 개발: "이걸 React로 다시 만들걸..."
React 프로젝트가 진행 중인 이유
현실:
- Alpine.js는 충분하지 않았음
- 복잡한 기능 추가 어려움
- 팀 확장 제한적
- 기술 부채 누적
해결책:
- React 병행 시작
- 새 기능은 React로
- 기존 Alpine은 유지
- 점진적 마이그레이션
최종 권장사항
우리의 상황 (2025년 12월 기준)
✅ 완료: Alpine.js 기본 기능
✅ 완료: PHP SSR + Alpine 하이브리드
✅ 진행 중: React 프로젝트 구축
⏳ 계획: 점진적 마이그레이션
실제 진척:
- Alpine.js: 80시간 투자, 50% 만족도
- React: 초기 단계, 기대도 높음
내가 해야 할 것
Step 1: React 프로젝트 완성 (2-3개월)
- 기본 레이아웃 완성
- 핵심 컴포넌트 구현
- 상태 관리 최적화
Step 2: Alpine.js와의 공존 관리 (3개월)
- 두 기술의 충돌 최소화
- 명확한 사용 기준 수립
- 팀 교육
Step 3: 선택적 마이그레이션 (6개월 이후)
- 복잡한 기능은 React로 변환
- 간단한 기능은 Alpine.js 유지
- 이중 성과 달성
팀에게
현재 상황:
- Alpine.js는 나쁜 선택이 아니었다
- 하지만 다음 단계로 가야 한다
- React는 이미 준비 중이다
좋은 소식:
- 우리는 두 기술 다 경험했다
- 각각의 장단점을 알고 있다
- 최적의 선택을 할 수 있다
- 기술 부채가 적다 (초기 단계에 발견)
최종 통계
Alpine.js 투자
개발 시간: 80시간
버그 이슈: 8개
해결율: 50%
재발생률: 50%
개발자 만족도: 6/10
기술 부채: 중간
React 프로젝트
개발 시간: 초기 단계
버그 이슈: 0개 (아직)
예상 개발율: 70%
개발자 만족도: 예상 8/10
기술 부채: 낮음
앞으로의 방향
선택지
Option 1: Alpine.js로 끝까지 간다
- 단기 빠름
- 장기 비용 높음
- 리스크 높음
Option 2: React로 완전 전환
- 초기 비용 높음
- 장기 효율적
- 확장성 우수
Option 3: 하이브리드 운영 (추천)
- 균형 잡힘
- 점진적 전환
- 리스크 최소화
우리의 선택: Option 3 (이미 진행 중)
마지막 말
"완벽한 기술은 없다. 문제를 푸는 과정에서 배우고, 그 경험으로 다음 선택을 한다. 우리는 지금 그 과정의 중간쯤에 있다."
80시간의 Alpine.js 개발은 낭비가 아니다. 그것이 React 프로젝트의 기반이 되었기 때문이다.
작성: 2025-12-04 참고자료: Alpine.js 8개월 경험, React 초기 단계 검토: 개발팀 전체
첫 번째 댓글을 작성해 보세요.