#React 4개의 스레드 ✕ 해제
이온디
이온디 1개월 전
실제 버그, 이슈, 해결 과정 정리 작성일: 2025년 12월 4일 주제: 6개월 Alpine.js 개발에서 만난 문제들과 React 프로젝트와의 실제 비교 대상: 기술 의사결정자, 개발팀 목차 Alpine.js 작업 과정 만난 문제들과 해결 React 프로젝트의 현황 실제 비교 분석 결론 ️ Alpine.js 작업 과정 Phase 1: HTMX에서 Alpine.js로 마이그레이션 (2개월) 목표 HTMX 완전 제거 Alpine.js로 상태 관리 전환 기존 페이지 호환… 실제 버그, 이슈, 해결 과정 정리 작성일: 2025년 12월 4일 주제: 6개월 Alpine.js 개발에서 만난 문제들과 React 프로젝트와의 실제 비교 대상: 기술 의사결정자, 개발팀 목차 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 초기 단계 검토: 개발팀 전체
이온디
이온디 1개월 전
SEO 불필요한 복잡한 상태 관리 페이지의 진짜 문제점 작성일: 2025년 12월 4일 대상: 마이페이지 버그를 겪고 있는 개발팀 관점: 기술적 필요성, 버그 해결, 개발 효율성 목차 현재 마이페이지 상황 Alpine.js로 구현했을 때의 문제점 실제 마이페이지 구조 분석 React로 해결되는 것들 구체적 마이그레이션 계획 결론 현재 마이페이지 상황 마이페이지의 역할 URL: /mypage 액션별: - 프로필 정보: 프로필 수정, 이미지 업로드 - 내가 쓴 글: 작성 게… SEO 불필요한 복잡한 상태 관리 페이지의 진짜 문제점 작성일: 2025년 12월 4일 대상: 마이페이지 버그를 겪고 있는 개발팀 관점: 기술적 필요성, 버그 해결, 개발 효율성 목차 현재 마이페이지 상황 Alpine.js로 구현했을 때의 문제점 실제 마이페이지 구조 분석 React로 해결되는 것들 구체적 마이그레이션 계획 결론 현재 마이페이지 상황 마이페이지의 역할 URL: /mypage 액션별: - 프로필 정보: 프로필 수정, 이미지 업로드 - 내가 쓴 글: 작성 게시글 목록 (무한 스크롤) - 내가 쓴 댓글: 댓글 목록 (무한 스크롤) - 북마크: 북마크한 글 목록 - 포인트 내역: 포인트 이력 조회 - 알림 설정: 알림 옵션 설정 - 채팅하기: 실시간 메시지 (별도 모듈) - 회원 탈퇴: 계정 삭제 - 로그아웃: 로그아웃 특징: SEO 불필요 (로그인 필수 페이지) 마이페이지의 복잡도 사이드바 메뉴 <nav x-data> <!-- 7개 메뉴 --> @click.prevent="$store.mypage.showSection('profile')" :class="{ 'active': $store.mypage.activeSection === 'profile' }" 섹션들 1. 프로필 정보 - 프로필 이미지 업로드 - 닉네임 수정 - 이메일 표시 (읽기 전용) - 휴대폰 수정 - 비밀번호 변경 2. 내가 쓴 글 - 게시글 목록 - 무한 스크롤 - 필터/정렬 3. 내가 쓴 댓글 - 댓글 목록 - 무한 스크롤 4. 북마크 - 북마크 목록 - 페이지네이션 5. 포인트 내역 - 포인트 이력 - 필터링 6. 알림 설정 - 체크박스 옵션 - 설정 저장 7. 채팅하기 - 별도 모듈 ($content 표시) Alpine.js로 구현했을 때의 문제점 Problem #1: Alpine Store의 한계 // 현재 구조 <nav x-data> @click.prevent="$store.mypage.showSection('profile')" :class="{ 'active': $store.mypage.activeSection === 'profile' }" </nav> <!-- 각 섹션 --> <section x-show="$store.mypage.activeSection === 'profile'" x-data="mypageProfile()"> 문제점 1. Store 관리 복잡 - 전역 상태와 로컬 상태 혼재 - 어디서 업데이트되는지 불명확 컴포넌트 독립성 약함 메뉴와 섹션이 느슨하게 결합 한 섹션의 버그가 다른 섹션에 영향 상태 동기화 문제 // 문제: 여러 곳에서 상태 업데이트 $store.mypage.showSection('profile') // 메뉴 클릭 activeSection = 'profile' // 직접 할당 updateActiveSection('profile') // 함수 호출 // 어떤 방식이 정당한지 불명확 Problem #2: 폼 상태 관리 혼란 프로필 정보 섹션의 문제 <!-- 현재 코드 --> <section x-data="mypageProfile()"> <form @submit="updateMemberInfo($event)"> <input type="text" name="user_name" value="{{ $form_name }}"> <input type="text" name="nick_name" value="{{ $form_nick }}"> <input type="email" name="email_address" value="{{ $form_email }}"> <!-- ... --> </form> </section> 문제점 초기값 vs 현재값 혼재 // 어디서 truth of source인가? - HTML value 속성 (PHP에서 렌더링) - x-data의 formData 객체 - 컴포넌트의 로컬 상태 // 세 가지가 동기화되지 않을 수 있음! 변경 감지 어려움 // 사용자가 입력하면? // 1. HTML 입력값 변경 // 2. Alpine이 감지? // 3. x-data에 반영? // 프로세스가 자동이 아님 저장 후 상태 관리 // 저장 버튼 클릭 후 // 1. API 호출 // 2. 응답 받음 // 3. 화면 업데이트? // 응답 데이터로 상태를 업데이트할지? // 원래 값으로 롤백할지? // 로컬 변경사항은? // 명확하지 않음! Problem #3: 파일 업로드 복잡성 // 현재 프로필 이미지 업로드 <input type="file" x-ref="profileImageInput" accept="image/*" @change="previewProfileImage($event)"> 문제점 // previewProfileImage 함수에서 해야 할 일 1. 파일 유효성 검사 - 파일 크기 (5MB 이상 거부) - 파일 타입 (JPG, PNG, GIF, WebP만) - 이미지 해상도 (너무 크면 거부) 2. 프리뷰 이미지 생성 - FileReader API 사용 - Base64로 변환 - 이미지 표시 3. 폼 상태 업데이트 - 원본 파일 저장 - 프리뷰 URL 저장 - 변경 상태 표시 4. 저장 로직 - FormData 생성 - 파일 업로드 - 서버 응답 처리 - 새 이미지 URL 반영 - 기존 이미지 삭제? 이 모든 것을 Alpine.js에서 관리하면? → x-data에 메서드 30개+ → 로직 이해 불가능 → 버그 발생 가능성 높음 Problem #4: 무한 스크롤 구현 복잡 // 내가 쓴 글/댓글 섹션 // 각각 무한 스크롤 필요 // Alpine.js로 구현: x-data="mypageProfile()" { items: [], currentPage: 1, loading: false, hasMore: true, async loadMore() { if (this.loading || !this.hasMore) return; this.loading = true; // API 호출 // 데이터 추가 // 로딩 상태 업데이트 this.loading = false; } } 문제점 중복 구현 boardList.js의 무한 스크롤과 동일 마이페이지에서 다시 구현? 버그도 중복 성능 문제 대량의 DOM 노드 가상 스크롤링 안 함 메모리 누수 위험 상태 관리 복잡 여러 섹션의 스크롤 상태 분리? 탭 전환 시 스크롤 위치 유지? 새로고침 시 데이터 복원? Problem #5: 검증 로직 부재 // 현재 닉네임 입력 <input type="text" name="nick_name" value="{{ $form_nick }}" required> 문제점 클라이언트 검증 없음 영문/숫자/특수문자 검사? 길이 제한 (2-20자)? 중복 검사 (실시간)? 서버 에러 처리 // 닉네임 중복이라고 서버가 응답하면? // 사용자에게 어떻게 표시? // 폼에 에러 메시지? // 모달? // Alpine.js에선 불명확 폼 전체 검증 // 저장 버튼 클릭 시 // 어떤 필드들을 검증? // 어떤 필드가 필수? // 에러 메시지는 어디에? Problem #6: 상태 불일치 시나리오 1. 사용자가 마이페이지 접속 2. 프로필 정보 로드 (닉네임: "user1") 3. 닉네임 수정 (입력: "user2") 4. 다른 탭으로 이동 5. 다시 프로필 탭으로 복귀 문제: 입력값이 유지되나? 초기값으로 돌아가나? Alpine.js: 불명확 (x-data 생성 방식에 따라 다름) 실제 마이페이지 구조 분석 Alpine.js 현재 코드 복잡도 파일: /layouts/el_d1/assets/pages/mypage.blade.php 구조: - 사이드바: nav x-data 1개 - 섹션들: 7개 x-data (각각 독립) - Store: $store.mypage 코드 라인수: - 현재: ~500줄 (아직 미완성) - 예상: ~800줄 (모든 섹션 완성 시) 문제점: - 각 섹션 x-data가 독립적 - 공유 로직 없음 (중복) - 상태 관리 분산 - 통신 방식 일관성 없음 필요한 기능들 1. 탭 전환 - Alpine.js: $store.mypage.activeSection 제어 2. 폼 입력 처리 - Alpine.js: value 바인딩, @change 이벤트 3. 파일 업로드 - Alpine.js: FileReader, FormData 관리 4. 무한 스크롤 - Alpine.js: 스크롤 이벤트 감지, API 호출 5. 실시간 검증 - Alpine.js: @change 이벤트에서 검증 6. 에러 표시 - Alpine.js: x-show로 에러 메시지 7. 로딩 상태 - Alpine.js: $store 또는 로컬 상태 모두 Alpine.js에서 직접 처리 → 복잡도 지수함수적 증가 ✅ React로 해결되는 것들 1️⃣ 상태 관리의 명확화 Alpine.js 문제 // 여러 곳에 상태가 분산됨 $store.mypage.activeSection // Store (공유) this.formData // x-data (로컬) this.loading // x-data (로컬) this.previewImage // x-data (로컬) // 어떤 상태가 어디서 관리되는지 불명확 React 해결 // 중앙 집중식 상태 관리 const [activeSection, setActiveSection] = useState('profile'); const [formData, setFormData] = useState({ user_name: '', nick_name: '', email: '', phone: '' }); const [loading, setLoading] = useState(false); const [previewImage, setPreviewImage] = useState(null); // 모든 상태가 명확함 // 어디서 업데이트되는지 추적 가능 // 타입 안전 (TypeScript) 장점: - 상태의 진실이 한 곳 (Single Source of Truth) - 업데이트 흐름이 명확 - 디버깅 쉬움 - 테스트 가능 2️⃣ 폼 상태 관리 표준화 Alpine.js 문제 // 문제: HTML value와 x-data 동기화 불명확 <input type="text" name="nick_name" value="{{ $form_nick }}"> // ↑ 초기값은 PHP에서, 변경은 Alpine에서? React 해결 // 표준 패턴 const [formData, setFormData] = useState({ nick_name: initialData.nick_name }); const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; return ( <input type="text" name="nick_name" value={formData.nick_name} onChange={handleInputChange} /> ); 장점: - 폼과 상태가 항상 동기화 (controlled component) - 변경 감지 자동 - 검증 로직 통합 가능 - 저장 후 상태 업데이트 명확 3️⃣ 파일 업로드 단순화 Alpine.js 문제 // previewProfileImage 함수에서 모든 로직 처리 async previewProfileImage(event) { // 1. 파일 검증 // 2. 프리뷰 생성 // 3. 상태 업데이트 // 4. 저장 로직 // → 메서드가 너무 복잡 } React 해결 // 작은 역할별 함수 분리 const validateFile = (file) => { if (file.size > 5 * 1024 * 1024) return '파일이 너무 큽니다'; if (!['image/jpeg', 'image/png'].includes(file.type)) { return '지원하지 않는 파일 형식입니다'; } return null; }; const createPreview = async (file) => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.readAsDataURL(file); }); }; const handleImageUpload = async (e) => { const file = e.target.files[0]; // 1. 검증 const error = validateFile(file); if (error) { setErrors(prev => ({ ...prev, image: error })); return; } // 2. 프리뷰 const preview = await createPreview(file); setPreviewImage(preview); // 3. 업로드 await uploadProfileImage(file); }; 장점: - 각 단계가 명확 - 함수 재사용 가능 - 테스트 쉬움 - 에러 처리 표준화 4️⃣ 무한 스크롤 라이브러리 활용 Alpine.js 문제 // 매번 처음부터 구현 async loadMore() { // DOM 전체 리렌더링 // 성능 최적화 안 함 // 가상 스크롤링 불가능 } React 해결 import { useInfiniteQuery } from '@tanstack/react-query'; const MyPostsSection = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['myPosts'], queryFn: fetchUserPosts, getNextPageParam: (lastPage) => lastPage.nextCursor }); const [ref] = useInView({ onInView: () => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); } } }); return ( <VirtualList items={data?.pages.flatMap(p => p.items) || []} renderItem={(item) => <PostCard post={item} />} onScrollToEnd={ref} /> ); }; 장점: - 라이브러리가 최적화 담당 - 가상 스크롤링 자동 - 캐싱 자동 - 성능 우수 5️⃣ 실시간 검증 Alpine.js 문제 // 검증 로직이 분산됨 @change="validateNickname($event)" @blur="checkNicknameDuplicate($event)" @submit="validateForm($event)" // 어떤 검증이 어디서 일어나는지 추적 불가 React 해결 import { useForm } from 'react-hook-form'; const ProfileForm = () => { const { register, watch, formState: { errors }, handleSubmit } = useForm({ mode: 'onBlur', // 모드 명확 resolver: profileFormResolver // 중앙 검증 함수 }); // 실시간 검증 const nickName = watch('nick_name'); const [isDuplicate, setIsDuplicate] = useState(false); useEffect(() => { if (nickName.length > 2) { checkNicknameDuplicate(nickName).then(setIsDuplicate); } }, [nickName]); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('nick_name', { required: '닉네임은 필수입니다', minLength: { value: 2, message: '최소 2자입니다' }, maxLength: { value: 20, message: '최대 20자입니다' } })} /> {errors.nick_name && <span>{errors.nick_name.message}</span>} {isDuplicate && <span>이미 사용 중인 닉네임입니다</span>} </form> ); }; 장점: - 검증 규칙이 명확 - 에러 메시지 관리 표준화 - 조건부 검증 쉬움 - 테스트 가능 6️⃣ 탭/섹션 관리 명확화 Alpine.js 문제 // 메뉴 클릭 @click.prevent="$store.mypage.showSection('profile')" // 섹션 표시 x-show="$store.mypage.activeSection === 'profile'" // 문제: showSection이 뭘 하는지 불명확 // activeSection이 어디서 변경되는지 추적 어려움 React 해결 // 명확한 탭 구조 const SECTIONS = { PROFILE: 'profile', POSTS: 'posts', COMMENTS: 'comments', BOOKMARKS: 'bookmarks' }; const MyPage = () => { const [activeSection, setActiveSection] = useState(SECTIONS.PROFILE); const handleSectionChange = (section) => { setActiveSection(section); // 섹션 변경 로직이 한 곳 }; return ( <> <Sidebar activeSection={activeSection} onSelect={handleSectionChange} /> <ProfileSection visible={activeSection === SECTIONS.PROFILE} /> <PostsSection visible={activeSection === SECTIONS.POSTS} /> {/* ... */} </> ); }; 장점: - 상태 흐름이 명확 - 컴포넌트 재사용 가능 - 테스트 쉬움 - Props drilling으로 명확한 의존성 7️⃣ 에러 처리 표준화 Alpine.js 문제 // 각각 다른 방식으로 에러 처리 try { // API 호출 } catch (error) { // x-show로 에러 표시? // alert 띄우기? // 모달 띄우기? // 불명확 } React 해결 const useFormSubmit = (onSuccess) => { const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const submit = async (data) => { setLoading(true); setError(null); try { const result = await submitForm(data); onSuccess(result); } catch (err) { setError(err.message); } finally { setLoading(false); } }; return { error, loading, submit }; }; // 사용 const ProfileForm = () => { const { error, loading, submit } = useFormSubmit(() => { showSuccessToast('프로필이 업데이트되었습니다'); }); return ( <form onSubmit={submit}> {error && <ErrorAlert message={error} />} {/* ... */} </form> ); }; 장점: - 에러 처리가 표준화됨 - 재사용 가능한 훅 - 전역 에러 관리 가능 - 테스트 쉬움 ️ 구체적 마이그레이션 계획 Phase 1: 기본 구조 (1주일) 목표: React 기본 마이페이지 구축 Step 1: 레이아웃 컴포넌트 - MyPageLayout.tsx - MyPageSidebar.tsx - MyPageContent.tsx Step 2: 탭 관리 - useActiveSection 훅 - 탭 상태 관리 Step 3: 프로필 섹션 - ProfileSection.tsx - 기본 폼 구조 Phase 2: 주요 기능 (2주일) Step 1: 파일 업로드 - 이미지 검증 - 프리뷰 - 업로드 로직 Step 2: 무한 스크롤 - 내 글 목록 - 내 댓글 목록 - useInfiniteQuery Step 3: 폼 검증 - react-hook-form - Zod 스키마 Phase 3: 상세 기능 (1주일) Step 1: 포인트 내역 Step 2: 북마크 Step 3: 알림 설정 Step 4: 채팅 연동 Phase 4: 테스트 및 최적화 (1주일) - 단위 테스트 - 통합 테스트 - 성능 최적화 - 모바일 반응형 총 기간: 5-6주 (부분 시간) 개발 효율 비교 마이페이지 개발 비용 Alpine.js로 완성하려면 1. 현재 (~500줄): 40시간 2. 파일 업로드: 8시간 3. 무한 스크롤: 10시간 4. 검증 로직: 8시간 5. 에러 처리: 5시간 6. 버그 수정: 20시간 (예상) ━━━━━━━━━━━━━━━━━━━━━ 총: 91시간 (약 2주) 버그 발생률: 높음 유지보수: 어려움 React로 개발하면 1. 구조 설계: 8시간 2. 기본 레이아웃: 12시간 3. 상태 관리: 10시간 4. 파일 업로드: 6시간 (라이브러리) 5. 무한 스크롤: 4시간 (react-query) 6. 검증: 4시간 (react-hook-form) 7. 테스트: 8시간 ━━━━━━━━━━━━━━━━━━━━━ 총: 52시간 (약 1주) 버그 발생률: 낮음 유지보수: 쉬움 비교 - Alpine.js: 91시간 + 20시간 (버그) - React: 52시간 (버그 거의 없음) - 절감: 59시간 (약 30% 효율) 결론 마이페이지는 React가 필수 이유들 1. SEO 불필요 - 로그인 필수 페이지 - 검색 엔진 크롤링 안 함 - React 선택에 제약 없음 ✅ 2. 복잡한 상태 관리 - 7개 섹션 - 각각 다른 상태 - 탭 전환 로직 - Alpine.js로는 버그 prone ❌ 3. 파일 처리 - 이미지 업로드 - 검증 - 프리뷰 - Alpine.js는 번거로움 ❌ 4. 무한 스크롤 - 여러 리스트 - 성능 중요 - React 라이브러리 최적 ✅ 5. 개발 효율 - 59시간 절감 - 버그 20시간 절감 - 유지보수 쉬움 ✅ 구체적 추천 ✅ DO: React로 마이페이지 개발 - SEO 불필요하니 자유도 높음 - 복잡한 상태 관리에 최적 - 라이브러리 활용으로 효율적 - 버그 감소, 유지보수 쉬움 ⏸️ HOLD: Alpine.js 다른 페이지 - QNA, Expert 등은 SEO 필요 - Alpine.js + PHP SSR 유지 MIGRATE: 기타 로그인 필수 페이지 - MyPage가 성공하면 - 다른 로그인 필수 페이지도 React로 - Admin 관리 페이지 - 대시보드 시간표 지금 (12월): 블로그 작성, 계획 수립 1월: React 마이페이지 개발 (Phase 1-2) 2월: Phase 3-4, 테스트, 배포 3월: 안정화, 사용자 피드백 최종 의견 마이페이지는 Alpine.js로 개발하면서 겪는 버그들이 기술의 한계가 아니라 잘못된 도구 선택이다. SEO가 불필요한 페이지에서 Alpine.js를 고집할 이유가 없다. React는 이런 복잡한 상태 관리를 위해 태어난 라이브러리다. 최소한 마이페이지는 React로 전환하자. 나머지는 그 결과를 보고 판단해도 된다. 작성: 2025-12-04 대상: 마이페이지 버그로 고민 중인 팀 메시지: "Alpine.js가 문제가 아니라, Alpine.js로는 하기 어려운 작업입니다"
이온디
이온디 1개월 전
TL;DR 25개 이상의 RESTful API 엔드포인트 React/Vue/Flutter 등 어디서든 사용 가능 CORS + 세션 기반 안전한 인증 레이아웃 페이지에서도 동작하는 댓글 시스템 게시판, 회원, 댓글, 추천, 마이페이지 완벽 지원 들어가며: React 스킨의 숙제 Rhymix로 React 게시판 스킨을 만들면서 가장 큰 벽은 무엇이었을까요? <!-- Rhymix 템플릿 엔진 --> <ul> <li loop="$document_list=>$document"… TL;DR 25개 이상의 RESTful API 엔드포인트 React/Vue/Flutter 등 어디서든 사용 가능 CORS + 세션 기반 안전한 인증 레이아웃 페이지에서도 동작하는 댓글 시스템 게시판, 회원, 댓글, 추천, 마이페이지 완벽 지원 들어가며: React 스킨의 숙제 Rhymix로 React 게시판 스킨을 만들면서 가장 큰 벽은 무엇이었을까요? <!-- Rhymix 템플릿 엔진 --> <ul> <li loop="$document_list=>$document"> {$document->getTitle()} </li> </ul> // React 컴포넌트 function BoardList() { const [documents, setDocuments] = useState([]); // 어떻게 데이터를 가져올까? } 템플릿 엔진과 React의 충돌 - SSR(서버 사이드 렌더링)과 CSR(클라이언트 사이드 렌더링)의 불일치 - 기존 proc* 액션들은 HTML 리다이렉트 방식 - JSON 데이터를 직접 가져올 방법이 없음 "JSON API가 있다면 얼마나 좋을까?" 이 절실한 필요에서 API 모듈이 탄생했습니다. API 모듈이 해결한 문제들 문제 1: React/Vue 스킨 개발의 어려움 기존 방식 // procBoardInsertComment 호출 exec_json('procBoardInsertComment', { document_srl: 123, content: '댓글' }); // → 게시판 모듈 컨텍스트가 없으면 실패 // → 레이아웃 페이지에서 사용 불가 API 모듈 방식 // REST API 호출 fetch('/modules/api/rest.php?type=comment_insert&document_srl=123', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: '댓글' }), credentials: 'same-origin' }); // → 어디서든 동작! // → 게시판 스킨, 레이아웃 페이지, 위젯 모두 OK 문제 2: 크로스 플랫폼 앱 개발 기존 방식 - Rhymix는 웹 전용 - 모바일 앱이나 데스크톱 앱 개발 어려움 - 데이터를 가져올 표준 방법 없음 API 모듈 방식 // 동일한 API를 모든 플랫폼에서 사용 const API = 'https://eond.com/modules/api/rest.php'; // React Native (모바일 앱) fetch(`${API}?type=document_list&mid=board`) // Flutter (iOS/Android) http.get(Uri.parse('${API}?type=document_list&mid=board')) // Electron (데스크톱 앱) fetch(`${API}?type=document_list&mid=board`) 하나의 API로 모든 플랫폼 지원! 문제 3: HTML 응답 vs JSON 응답 기존 Rhymix proc* 액션 // procBoardInsertDocument 호출 const response = await fetch('/index.php', { method: 'POST', body: formData }); // 응답: HTML 페이지 (리다이렉트) // JSON 파싱 시도 → SyntaxError 발생! API 모듈 // REST API 호출 const response = await fetch('/modules/api/rest.php?type=document_insert', { method: 'POST', body: JSON.stringify(data) }); // 응답: 항상 JSON const result = await response.json(); // { // "status": 1, // "message": "성공", // "data": { "document_srl": 123 } // } 일관된 JSON 응답 형식! API 모듈의 완벽한 기능 1. 게시판 API 게시글 목록 조회 GET /api?mid=board&act=dispApiDocumentList&page=1 // 파라미터 - page: 페이지 번호 - list_count: 페이지당 개수 - search_target: 검색 대상 (title, content, nick_name) - search_keyword: 검색 키워드 - category_srl: 카테고리 필터 - sort_index: 정렬 기준 (list_order, regdate, readed_count) - order_type: 정렬 방향 (asc, desc) 응답 예시 { "status": 1, "message": "성공", "payload": { "documents": [ { "document_srl": 123, "title": "게시글 제목", "content": "게시글 요약...", "nick_name": "작성자", "regdate": "2024-11-10 12:00:00", "readed_count": 100, "comment_count": 5, "voted_count": 10, "thumbnail": "https://..." } ], "total_count": 150, "total_page": 8, "page": 1, "list_count": 20, "categories": [...], "grant": { "list": true, "view": true, "write_document": true, "write_comment": true } } } 게시글 상세 조회 GET /api?mid=board&act=dispApiDocument&document_srl=123 // 파라미터 - document_srl: 게시글 번호 (필수) - skip_view_count: true면 조회수 증가 안 함 응답 예시 { "status": 1, "message": "성공", "payload": { "document": { "document_srl": 123, "title": "게시글 제목", "content": "게시글 전체 내용...", "nick_name": "작성자", "user_id": "userid", "regdate": "2024-11-10 12:00:00", "readed_count": 101, "comment_count": 5, "voted_count": 10, "category_srl": 1, "tags": ["React", "Rhymix", "API"] }, "comments": [...], "files": [...], "prev_document": { "document_srl": 122, "title": "이전글 제목" }, "next_document": { "document_srl": 124, "title": "다음글 제목" }, "grant": { "view": true, "write_comment": true, "is_granted": false } } } 스마트 이전글/다음글 필터링 API 모듈은 카테고리와 태그를 기반으로 관련된 이전글/다음글을 찾습니다: 현재 문서: - 카테고리: "프로젝트" - 태그: "React, TypeScript, API" 1차 시도 (필터링): → 같은 카테고리 + 공통 태그가 있는 문서 2차 시도 (Fallback): → 1차에서 결과 없으면 전체 게시판에서 찾기 게시글 작성 POST /modules/api/rest.php?type=document_insert // Body (JSON) { "mid": "board", "title": "게시글 제목", "content": "게시글 내용", "category_srl": 1, // 선택 "is_secret": "N", // 비밀글 여부 "tags": "React,API" // 선택 } // 응답 { "status": 1, "message": "게시글이 등록되었습니다.", "data": { "document_srl": 123 } } 게시글 수정/삭제 // 수정 POST /modules/api/rest.php?type=document_update { "document_srl": 123, "title": "수정된 제목", "content": "수정된 내용" } // 삭제 POST /modules/api/rest.php?type=document_delete { "document_srl": 123 } 2. 댓글 API 왜 레이아웃 페이지에서도 동작하나? 핵심은 독립적인 엔드포인트와 executeQuery 직접 사용입니다. // API 모듈의 댓글 등록 (rest.php) case 'comment_insert': // 1. 세션에서 로그인 정보 가져오기 $logged_info = Context::get('logged_info'); // 2. executeQuery로 직접 DB에 삽입 $output = executeQuery( 'insertComment', $comment_args ); // 3. JSON 응답 반환 echo json_encode([ 'status' => 1, 'message' => '댓글이 등록되었습니다.', 'data' => ['comment_srl' => $comment_srl] ]); // → 게시판 모듈 컨텍스트 불필요! 댓글 작성 POST /modules/api/rest.php?type=comment_insert&document_srl=123 // Body (JSON) { "content": "댓글 내용", "parent_srl": 0 // 대댓글이면 부모 댓글 번호 } // 응답 { "status": 1, "message": "댓글이 등록되었습니다.", "data": { "comment_srl": 456 } } 보안 체크 - ✅ 로그인 필수 (세션 기반) - ✅ 게시글 존재 여부 확인 - ✅ 댓글 권한 체크 - ✅ SQL Injection 방지 (prepared statement) - ✅ XSS 방지 (출력 이스케이프) 댓글 수정/삭제 // 수정 POST /modules/api/rest.php?type=comment_update { "comment_srl": 456, "content": "수정된 내용" } // 삭제 POST /modules/api/rest.php?type=comment_delete { "comment_srl": 456 } 권한 체크 - 댓글 작성자 또는 관리자만 수정/삭제 가능 3. 추천/비추천 API // 게시글 추천 POST /modules/api/rest.php?type=vote_up&document_srl=123 // 게시글 비추천 POST /modules/api/rest.php?type=vote_down&document_srl=123 // 추천/비추천 취소 POST /modules/api/rest.php?type=vote_cancel&document_srl=123 // 응답 { "status": 1, "message": "추천하였습니다.", "data": { "voted_count": 11 // 현재 추천 수 } } 4. 회원 인증 API 비밀번호 찾기 POST /modules/api/rest.php?type=password_reset_request // Body (JSON) { "user_id": "사용자아이디", "email_address": "user@example.com" } // 응답 { "status": 1, "message": "비밀번호 재설정 링크가 이메일로 발송되었습니다." } 동작 방식 1. 아이디와 이메일 주소 일치 확인 2. 1시간 유효한 인증 토큰 생성 3. 이메일로 재설정 링크 발송 회원가입 POST /modules/api/rest.php?type=member_signup // Body (JSON) { "user_id": "userid", "password": "비밀번호123", "password_confirm": "비밀번호123", "email_address": "user@example.com", "nick_name": "닉네임", "user_name": "이름", "allow_mailing": "Y", // 메일 수신 동의 "allow_message": "Y" // 쪽지 수신 동의 } // 응답 { "status": 1, "message": "회원가입이 완료되었습니다.", "data": { "member_srl": 12345, "require_confirm": false // 이메일 인증 필요 여부 } } 검증 - 비밀번호: 최소 8자, 영문+숫자 포함 - 아이디, 이메일, 닉네임 중복 체크 - 이메일 인증 설정 시 인증 메일 발송 5. 마이페이지 API 내 정보 조회 GET /modules/api/rest.php?type=member_my_info // 응답 { "status": 1, "message": "성공", "data": { "member_srl": 12345, "user_id": "userid", "nick_name": "닉네임", "user_name": "홍길동", "email_address": "user@example.com", "profile_image": "https://eond.com/files/member_extra_info/profile.jpg", "regdate": "2024-01-15 10:00:00", "last_login": "2025-12-03 09:30:00", "point": 1500, "level": 5 } } 포인트 히스토리 GET /modules/api/rest.php?type=member_point_history&page=1 // 응답 { "status": 1, "message": "성공", "data": { "current_point": 1500, "history": [ { "point_srl": 789, "point": 100, // 증감량 (+ 적립, - 차감) "accumulated_point": 1500, // 해당 시점 누적 "comment": "게시글 작성", "regdate": "2025-12-03 10:00:00" }, { "point_srl": 788, "point": -50, "accumulated_point": 1400, "comment": "댓글 작성", "regdate": "2025-12-02 15:30:00" } ], "total_count": 150, "total_page": 8, "page": 1 } } 프로필 이미지 변경 POST /modules/api/rest.php?type=member_update_profile_image Content-Type: multipart/form-data FormData: profile_image: [이미지 파일] // 응답 { "status": 1, "message": "프로필 이미지가 변경되었습니다.", "data": { "profile_image": "https://eond.com/files/member_extra_info/..." } } 제한 - 파일 형식: JPG, PNG, GIF, WebP - 최대 크기: 5MB 비밀번호 변경 POST /modules/api/rest.php?type=member_update_password // Body (JSON) { "current_password": "현재비밀번호", "new_password": "새비밀번호123", "new_password_confirm": "새비밀번호123" } // 응답 { "status": 1, "message": "비밀번호가 변경되었습니다." } 개인정보 수정 POST /modules/api/rest.php?type=member_update_info // Body (JSON) { "nick_name": "새닉네임", "user_name": "새이름", "email_address": "new@example.com", "allow_mailing": "Y", "allow_message": "N" } // 응답 { "status": 1, "message": "개인정보가 수정되었습니다.", "data": { "member_srl": 12345, "user_id": "userid", "nick_name": "새닉네임", "user_name": "새이름", "email_address": "new@example.com", "profile_image": "..." } } 활동 내역 조회 // 내가 쓴 글 GET /modules/api/rest.php?type=member_my_documents&page=1&list_count=20&mid=board // 내가 쓴 댓글 GET /modules/api/rest.php?type=member_my_comments&page=1&list_count=20 // 스크랩한 글 GET /modules/api/rest.php?type=member_my_scraps&page=1&list_count=20 6. 인기글 API GET /modules/api/rest.php?type=popular_documents &mid=board &page=1 &list_count=20 &period=7 &sort_by=readed_count // 파라미터 - mid: 게시판 mid (필수) - page: 페이지 번호 (기본 1) - list_count: 페이지당 개수 (기본 20) - period: 기간 (일 단위, 기본 7) - sort_by: 정렬 기준 * readed_count: 조회수 (기본) * voted_count: 추천수 * comment_count: 댓글수 // 응답 { "status": 1, "message": "성공", "data": { "documents": [ { "document_srl": 123, "title": "인기글 제목", "content": "요약...", "nick_name": "작성자", "regdate": "2025-12-01 10:00:00", "readed_count": 1000, "voted_count": 50, "comment_count": 30, "thumbnail": "https://..." } ], "total_count": 100, "total_page": 5, "page": 1, "period": 7, "sort_by": "readed_count" } } 실전 활용 예시 예시 1: React 게시판 스킨 // TypeScript + React Hooks import React, { useState, useEffect } from 'react'; interface Document { document_srl: number; title: string; content: string; nick_name: string; regdate: string; readed_count: number; comment_count: number; } function BoardList({ mid }: { mid: string }) { const [documents, setDocuments] = useState<Document[]>([]); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); useEffect(() => { fetchDocuments(); }, [page]); const fetchDocuments = async () => { try { const response = await fetch( `/api?mid=${mid}&act=dispApiDocumentList&page=${page}` ); const data = await response.json(); if (data.status === 1) { setDocuments(data.payload.documents); } } catch (error) { console.error('Error fetching documents:', error); } finally { setLoading(false); } }; if (loading) { return <div className="loading">로딩 중...</div>; } return ( <div className="board-list"> {documents.map(doc => ( <article key={doc.document_srl} className="document-item"> <h2> <a href={`/board/${doc.document_srl}`}> {doc.title} </a> </h2> <p className="content">{doc.content}</p> <div className="meta"> <span className="author">{doc.nick_name}</span> <span className="date">{doc.regdate}</span> <span className="views">조회 {doc.readed_count}</span> <span className="comments">댓글 {doc.comment_count}</span> </div> </article> ))} <div className="pagination"> <button onClick={() => setPage(p => p - 1)} disabled={page === 1}> 이전 </button> <span>페이지 {page}</span> <button onClick={() => setPage(p => p + 1)}> 다음 </button> </div> </div> ); } export default BoardList; 예시 2: 레이아웃 페이지에 댓글 추가 // 홈페이지(레이아웃 페이지)에 댓글 시스템 추가 // 댓글 목록 로드 async function loadComments(documentSrl) { const response = await fetch( `/api?mid=notice&act=dispApiDocument&document_srl=${documentSrl}` ); const data = await response.json(); if (data.status === 1) { displayComments(data.payload.comments); } } // 댓글 작성 async function submitComment(documentSrl, content) { const response = await fetch( `/modules/api/rest.php?type=comment_insert&document_srl=${documentSrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: content }), credentials: 'same-origin' // 쿠키(세션) 전송 필수! } ); const result = await response.json(); if (result.status === 1) { alert('댓글이 등록되었습니다.'); loadComments(documentSrl); // 댓글 목록 새로고침 } else { alert('댓글 등록 실패: ' + result.message); } } // 사용 예시 document.getElementById('comment-form').addEventListener('submit', (e) => { e.preventDefault(); const content = document.getElementById('comment-content').value; const documentSrl = 123; // 게시글 번호 submitComment(documentSrl, content); }); 예시 3: 모바일 앱 (React Native) // React Native로 Rhymix 게시판 앱 만들기 import React, { useState, useEffect } from 'react'; import { View, Text, FlatList, TouchableOpacity } from 'react-native'; const API_URL = 'https://eond.com/modules/api/rest.php'; function BoardScreen() { const [documents, setDocuments] = useState([]); useEffect(() => { fetchDocuments(); }, []); const fetchDocuments = async () => { try { const response = await fetch( `https://eond.com/api?mid=board&act=dispApiDocumentList` ); const data = await response.json(); setDocuments(data.payload.documents); } catch (error) { console.error(error); } }; const vote = async (documentSrl) => { const response = await fetch( `${API_URL}?type=vote_up&document_srl=${documentSrl}`, { method: 'POST' } ); const result = await response.json(); alert(result.message); }; return ( <FlatList data={documents} keyExtractor={item => item.document_srl.toString()} renderItem={({ item }) => ( <View style={styles.item}> <Text style={styles.title}>{item.title}</Text> <Text>{item.content}</Text> <TouchableOpacity onPress={() => vote(item.document_srl)}> <Text> 추천 {item.voted_count}</Text> </TouchableOpacity> </View> )} /> ); } 보안 및 성능 보안 기능 1. CORS (Cross-Origin Resource Sharing) // 허용된 도메인만 API 접근 가능 $allowed_origins = [ 'https://eond.com', 'http://localhost:3000' // 개발용 ]; 2. 세션 기반 인증 // 쿠키(세션) 전송 필수 fetch('/modules/api/rest.php?type=comment_insert', { method: 'POST', credentials: 'same-origin' // 중요! }); 3. SQL Injection 방지 // executeQuery = PDO prepared statement $output = executeQuery('insertComment', $args); // → 자동으로 이스케이프 처리 4. XSS 방지 // 출력 시 자동 이스케이프 $comment->content = htmlspecialchars($content); 5. 권한 체크 // 댓글 수정/삭제 시 작성자 확인 if (!$oComment->isGranted()) { return ['status' => 0, 'message' => '권한이 없습니다.']; } 성능 최적화 1. 효율적인 쿼리 <!-- getDocumentList.xml --> <query> SELECT document_srl, title, content, nick_name, regdate FROM documents WHERE module_srl = #{module_srl} ORDER BY list_order ASC LIMIT #{list_count} </query> <!-- 필요한 컬럼만 조회 --> 2. 페이징 처리 // 대용량 데이터도 빠른 응답 GET /api?mid=board&page=1&list_count=20 // → 20개씩만 조회 3. JSON 직렬화 최적화 // 불필요한 데이터 제거 unset($document->variables); unset($document->_filter); echo json_encode($data, JSON_UNESCAPED_UNICODE); 설치 및 시작하기 시스템 요구사항 필수 - Rhymix 2.0 이상 - PHP 7.4 이상 - PDO 확장 모듈 - JSON 지원 권장 - PHP 8.0 이상 - HTTPS 환경 - Gzip 압축 활성화 설치 방법 1. 모듈 다운로드 # Git으로 다운로드 git clone https://github.com/eond/api.git modules/api # 또는 ZIP 파일 다운로드 후 압축 해제 2. 관리자 설정 1. 관리자 페이지 접속 http://yoursite.com/index.php?module=admin&act=dispApiAdminConfig 2. REST API 설정 - CORS 도메인 추가 - API 활성화 3. 저장 3. API 테스트 # 게시글 목록 조회 curl http://yoursite.com/api?mid=board&act=dispApiDocumentList # 응답 확인 { "status": 1, "message": "성공", "payload": { ... } } 개발 환경 설정 React 프로젝트에서 사용 // src/api/board.js const API_BASE = '/api'; const REST_API = '/modules/api/rest.php'; export const boardAPI = { // 게시글 목록 getDocuments: async (mid, page = 1) => { const response = await fetch( `${API_BASE}?mid=${mid}&act=dispApiDocumentList&page=${page}` ); return response.json(); }, // 댓글 작성 addComment: async (documentSrl, content) => { const response = await fetch( `${REST_API}?type=comment_insert&document_srl=${documentSrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }), credentials: 'same-origin' } ); return response.json(); } }; 버전 히스토리 v1.1.0 (2024-11-10) - 최신 ✨ 마이페이지 API 추가 내 정보 조회 포인트 히스토리 프로필 이미지 변경 비밀번호 변경 개인정보 수정 내가 쓴 글/댓글 목록 스크랩한 글 목록 ✨ 회원 인증 API 추가 비밀번호 찾기 회원가입 ✨ 인기글 API 추가 v1.0.0 (2024-10-01) API 모듈 첫 출시 ✨ 게시판 API (목록, 상세, 작성, 수정, 삭제) ✨ 댓글 API (작성, 수정, 삭제) ✨ 추천/비추천 API ✨ CORS 지원 ✨ 세션 기반 인증 로드맵 v1.2.0 (예정) [ ] GraphQL 지원 [ ] 웹소켓 실시간 알림 [ ] 파일 업로드 API [ ] 배치 작업 API v2.0.0 (장기) [ ] OAuth 2.0 인증 [ ] API 버전 관리 [ ] Rate Limiting [ ] API 문서 자동 생성 FAQ Q1. 기존 게시판 스킨과 호환되나요? A. 네, API 모듈은 기존 게시판 스킨과 독립적으로 동작합니다. 기존 스킨을 유지하면서 새로운 React 스킨을 추가로 개발할 수 있습니다. Q2. 모바일 앱에서 사용할 수 있나요? A. 네, React Native, Flutter, Swift, Kotlin 등 모든 플랫폼에서 사용 가능합니다. RESTful API 표준을 따르므로 HTTP 요청만 가능하면 됩니다. Q3. 보안은 안전한가요? A. 네, CORS, 세션 인증, SQL Injection 방지, XSS 방지 등 모든 보안 기능이 구현되어 있습니다. PDO prepared statement로 DB를 안전하게 처리합니다. Q4. 성능은 어떤가요? A. 효율적인 쿼리와 JSON 직렬화로 빠른 응답 속도를 보장합니다. 페이징 처리로 대용량 데이터도 문제없습니다. Q5. 라이선스는? A. 상업용 유료 라이선스입니다. 구매 후 소스코드, 기술 지원, 업데이트를 제공받으실 수 있습니다. 자세한 사항은 https://eond.com 또는 admin@eond.com으로 문의해주세요. Q6. 커스텀 API를 추가할 수 있나요? A. 네, rest.php에 새로운 case를 추가하면 됩니다. 기존 API를 참고하여 쉽게 확장할 수 있습니다. 사용자 후기 (테스트 사용자) "React로 게시판을 만들고 싶었는데, 데이터를 가져올 방법이 없어서 포기했었어요. API 모듈 덕분에 드디어 해냈습니다!" — React 개발자 K "레이아웃 페이지에서 댓글이 안 돼서 고민했는데, REST API로 간단하게 해결됐어요. 정말 편합니다." — 커뮤니티 관리자 L "Flutter로 모바일 앱을 만들고 있는데, API 모듈이 없었다면 불가능했을 겁니다. 감사합니다!" — 앱 개발자 P 라이선스 및 구매 API 모듈은 상업용 유료 라이선스 제품입니다. 구매 정보 - 가격: 문의 필요 - 구매 문의: https://eond.com 또는 admin@eond.com - 포함 사항: 소스코드, 기술 지원, 업데이트 지원 서비스 - 설치 및 설정 지원 - 기술 지원 (이메일, 포럼) - 무료 업데이트 (1년간) - 사용자 가이드 및 API 문서 제공 결론 API 모듈은 Rhymix의 가능성을 확장합니다. API 모듈이 제공하는 가치 ✅ React/Vue 스킨 개발: 완벽한 JSON API ✅ 크로스 플랫폼: 웹, 모바일, 데스크톱 앱 ✅ 독립적인 엔드포인트: 어디서든 동작 ✅ 표준 준수: RESTful API ✅ 보안: CORS + 세션 인증 지금 바로 시작하세요 React로 게시판을 만들고 싶으셨나요? 레이아웃 페이지에 댓글을 추가하고 싶으셨나요? 모바일 앱을 개발하고 싶으셨나요? API 모듈이 모든 것을 가능하게 합니다. 다운로드: https://github.com/eond/api API 문서: modules/api/README.md 데모: https://demo.eond.com/api 문의: admin@eond.com 작성일: 2025-12-03 카테고리: Rhymix, 모듈, API 태그: #API #Rhymix #JSON #RESTful #React #Vue #모바일앱 버전: API 모듈 v1.1.0
이온디
이온디 6개월 전
# MenuPro: Rhymix 사이트맵 관리의 새로운 표준 ## TL;DR - 🚀 메뉴 100개를 10분 만에 재배치 (기존 1시간 → 10분) - 🎯 드래그 앤 드롭으로 사이트맵 간 자유로운 이동 - ✨ React 기반 현대적인 UI/UX - 💾 일괄 저장으로 실수 방지 - 🔍 강력한 메뉴 검색 기능 --- <메뉴 아이템 수정부터 권한설정, 디자인설정까지 모두 하나의 화면에서> ## 들어가며: 왜 MenuPro를 만들었나? Rhymix 관리자라… # MenuPro: Rhymix 사이트맵 관리의 새로운 표준 ## TL;DR - 🚀 메뉴 100개를 10분 만에 재배치 (기존 1시간 → 10분) - 🎯 드래그 앤 드롭으로 사이트맵 간 자유로운 이동 - ✨ React 기반 현대적인 UI/UX - 💾 일괄 저장으로 실수 방지 - 🔍 강력한 메뉴 검색 기능 --- <메뉴 아이템 수정부터 권한설정, 디자인설정까지 모두 하나의 화면에서> ## 들어가며: 왜 MenuPro를 만들었나? Rhymix 관리자라면 누구나 공감할 이 순간: ``` 메뉴 하나 옮기기 → 자동 저장 → 페이지 새로고침 → 다시 메뉴 찾기 → 또 하나 옮기기 → 자동 저장 → 페이지 새로고침 → ...반복... ``` 10개의 메뉴를 정리하려면 10번의 새로고침. 100개의 메뉴를 재구성하려면? 상상만 해도 지칩니다. **"더 나은 방법이 있지 않을까?"** 이 질문에서 MenuPro가 탄생했습니다. --- <사이트맵 메뉴별로 디자인 설정을 쉽게 할 수 있다> ## MenuPro가 해결한 문제들 ### 문제 1: 반복적인 새로고침 **기존 방식** - 메뉴 하나 이동 → 자동 저장 → 새로고침 - 메뉴 100개 정리 = 새로고침 100번 - 소요 시간: 약 1시간 **MenuPro 방식** - 메뉴 100개를 자유롭게 드래그 앤 드롭 - 모두 정리한 후 "저장" 버튼 클릭 1회 - 소요 시간: 약 10분 **생산성 6배 향상!** ⚡ ### 문제 2: 복잡한 구조 파악의 어려움 **기존 방식** ``` 게시판 ├ 공지사항 ├ 자유게시판 └ FAQ 커뮤니티 ├ 갤러리 └ Q&A ``` 어떤 메뉴가 어디에 속하는지 한눈에 보기 어려웠습니다. **MenuPro 방식** <img src="screenshot_tree_structure.png" alt="트리 구조 스크린샷" /> - 📊 시각적 트리 구조 - 🔽 접기/펼치기로 원하는 뎁스만 보기 - 🎨 계층별 들여쓰기와 연결선 - 🏷️ 메뉴 타입 아이콘 표시 ### 문제 3: 사이트맵 간 이동의 번거로움 **기존 방식** 1. 메뉴 A를 사이트맵 1에서 삭제 2. 사이트맵 2로 이동 3. 메뉴 A를 다시 생성 4. 모든 설정을 다시 입력 **MenuPro 방식** 1. 메뉴 A를 드래그 2. 사이트맵 2로 드롭 3. 끝! ✨ 모든 설정(권한, 스킨, 레이아웃)이 그대로 유지됩니다. ### 문제 4: 메뉴 찾기의 어려움 사이트가 커질수록 메뉴를 찾기가 점점 어려워집니다. **MenuPro의 검색 기능** ``` 검색어 입력: "공지" → "공지사항" 메뉴 하이라이트 → 해당 사이트맵 자동 펼침 → 메뉴까지 자동 스크롤 ``` - 🔍 메뉴명으로 검색 - 🎯 mid(모듈 ID)로 검색 - ⚡ 실시간 검색 결과 --- <모든 사이트맵을 접어서 이동 가능한 사이트맵 메뉴> ## MenuPro의 핵심 기능 ### 1. 드래그 앤 드롭 시스템 **3가지 드래그 타입 지원** #### 타입 1: 같은 사이트맵 내 순서 변경 ``` [사이트맵 A] - 메뉴1 - 메뉴2 ← 드래그 - 메뉴3 ← 여기로 드롭 [결과] - 메뉴1 - 메뉴3 - 메뉴2 ``` #### 타입 2: 계층 구조 변경 ``` [드래그 전] - 메뉴1 - 메뉴2 └ 메뉴2-1 [메뉴1을 메뉴2 아래로 드래그] [드래그 후] - 메뉴2 ├ 메뉴2-1 └ 메뉴1 ← 2차 메뉴가 됨 ``` #### 타입 3: 사이트맵 간 이동 ``` [사이트맵 A] [사이트맵 B] - 메뉴1 - 메뉴A - 메뉴2 ──드래그──→ - 메뉴B - 메뉴3 [결과] [사이트맵 A] [사이트맵 B] - 메뉴1 - 메뉴A - 메뉴3 - 메뉴B - 메뉴2 ← 이동됨 ``` **하위 메뉴 자동 이동** - 메뉴2를 이동하면 메뉴2의 모든 하위 메뉴도 함께 이동 - 계층 구조 완벽 유지 <부모를 잃어버린 사이트맵 메뉴 찾기> ### 2. 일괄 저장 시스템 **변경 내역 실시간 추적** ``` [변경 내역 패널] ✓ 메뉴1: 순서 변경 (3 → 5) ✓ 메뉴2: 사이트맵 이동 (A → B) ✓ 메뉴3: 계층 변경 (1차 → 2차) ✓ 사이트맵 A: 순서 변경 [저장] [취소] [되돌리기] ``` - 📝 모든 변경사항 리스트로 표시 - ↩️ 저장 전 언제든 되돌리기 - 💾 한 번의 클릭으로 모두 적용 - ❌ 취소 버튼으로 전체 복원 **실수 방지 기능** - 저장 전 확인 대화상자 - 중요한 변경(삭제 등)은 2단계 확인 - 변경 내역 미리보기 ### 3. 사이트맵 관리 **사이트맵 순서 변경** ``` [드래그 앤 드롭으로 순서 변경] 메인 메뉴 서브 메뉴 ↓ ↑ ↓ ↑ 푸터 메뉴 모바일 메뉴 ``` - 🖱️ 사이트맵 헤더 드래그로 순서 변경 - 💾 변경 즉시 저장 - 🔄 언제든 순서 재배치 **사이트맵 생성/편집/삭제** ``` [사이트맵 생성] 이름: 새 사이트맵 설명: (선택사항) [생성] [사이트맵 편집] 이름 클릭 → 인라인 수정 → Enter ``` - ➕ 빠른 사이트맵 생성 - ✏️ 이름 클릭으로 즉시 수정 - 🗑️ 삭제 시 하위 메뉴 처리 선택 ### 4. 메뉴 검색 기능 **강력한 검색 옵션** ``` [검색창] 🔍 메뉴 찾기... 검색 대상: ☑ 메뉴명 ☑ mid(모듈 ID) ☑ 설명 검색 결과: 📌 공지사항 (mid: notice) 📌 공지사항 > 중요공지 (mid: notice_important) ``` **검색 결과 처리** - 🎯 검색된 메뉴 하이라이트 - 📂 해당 사이트맵 자동 펼침 - 📜 메뉴 위치로 자동 스크롤 - 🔢 검색 결과 개수 표시 ### 5. 메뉴 아이템 관리 **메뉴 생성** ``` [메뉴 추가] 메뉴 타입 선택: ○ 위젯 페이지 ○ 문서 페이지 ○ 게시판 ○ 외부 페이지 ● 바로가기 [다음 단계로] ``` **지원하는 메뉴 타입** - 📄 위젯 페이지 - 📝 문서 페이지 - 📋 게시판 - 🔗 외부 페이지 - ⚡ 바로가기 **메뉴 편집** ``` [메뉴 편집 사이드바] 기본 정보 ├ 메뉴명: 공지사항 ├ mid: notice └ 설명: 중요한 공지를 올리는... 디자인 ├ 레이아웃: el_basic ├ PC 스킨: board_default └ 모바일 스킨: mobile_default 권한 ├ 목록 권한: 전체 ├ 글쓰기 권한: 회원 └ 댓글 권한: 회원 고급 설정 ├ 새 창에서 열기: □ └ 확장 상태: □ ``` **메뉴 삭제** ``` [메뉴 삭제 확인] "공지사항" 메뉴를 삭제하시겠습니까? 연결된 모듈도 함께 삭제하시겠습니까? ○ 메뉴만 삭제 (모듈은 유지) ● 모듈도 함께 삭제 (복구 불가) [취소] [삭제] ``` --- <실수로 연결이 끊어진 메뉴도 자동으로 제 자리를 찾아줍니다> ## 기술 스택: 현대적인 아키텍처 ### 프론트엔드 **React 18 + TypeScript** ```typescript // 타입 안정성과 최신 React 기능 interface MenuItem { menu_item_srl: number; name: string; url: string; parent_srl: number; // ... } const MenuGrid: React.FC<MenuGridProps> = ({ menus, onSave }) => { // Hooks 기반 상태 관리 const [selectedMenu, setSelectedMenu] = useState<MenuItem | null>(null); // ... }; ``` **주요 라이브러리** - **react-complex-tree**: 트리 구조 및 드래그 앤 드롭 - **@dnd-kit**: 사이트맵 순서 변경 - **shadcn/ui**: 세련된 UI 컴포넌트 - **Webpack 5**: 최적화된 번들링 **왜 React인가?** - ⚡ 빠른 렌더링 (Virtual DOM) - 🔄 효율적인 상태 관리 - 🧩 컴포넌트 재사용 - 📦 풍부한 생태계 ### 백엔드 **Rhymix 2.1.8+ 네임스페이스 구조** ```php namespace Rhymix\Modules\Menupro\Controllers; class Admin extends Base { public function procMenuproAdminUpdateMenuItemsOrder() { // PSR-4 자동로드 // 명확한 네임스페이스 // 현대적인 PHP 코드 } } ``` **MVC 패턴** ``` modules/menupro/ ├── controllers/ # 요청 처리 │ ├── Admin.php # 관리자 액션 │ ├── Index.php # API 엔드포인트 │ └── Install.php # 설치/업데이트 ├── models/ # 비즈니스 로직 │ └── Menu.php # 메뉴 데이터 관리 ├── views/ # 화면 템플릿 │ └── admin/ │ └── config.blade.php └── conf/ ├── info.xml # 모듈 정보 └── module.xml # 액션 정의 ``` **Blade v2 템플릿** ```blade @version(2) <div class="menupro-container"> @if($menus) @foreach($menus as $menu) <div class="menu-item"> {{ $menu->title }} </div> @endforeach @else <p>등록된 사이트맵이 없습니다.</p> @endif </div> ``` --- <다중 선택 이동 기능> ## 실전 활용 시나리오 ### 시나리오 1: 대형 포털 사이트 메뉴 재구성 **상황** - 100개의 메뉴가 있는 커뮤니티 사이트 - 카테고리별로 메뉴 그룹 재구성 필요 - 3개 사이트맵 → 5개 사이트맵으로 분리 **기존 방식 (약 2시간)** ``` 1. 새 사이트맵 2개 생성 (10분) 2. 메뉴 하나씩 이동 (100번 × 1분 = 100분) 3. 순서 정리 (30분) 4. 테스트 및 수정 (20분) ``` **MenuPro 방식 (약 20분)** ``` 1. 새 사이트맵 2개 생성 (2분) 2. 메뉴 드래그 앤 드롭으로 일괄 이동 (10분) 3. 순서 드래그로 정리 (5분) 4. 한 번에 저장 (1분) 5. 테스트 (2분) ``` **생산성 6배 향상!** ### 시나리오 2: 모바일/PC 메뉴 분리 **상황** - 기존: PC와 모바일이 같은 메뉴 사용 - 목표: 모바일 전용 간소화 메뉴 구성 **MenuPro로 쉽게 해결** ``` 1. "모바일 메뉴" 사이트맵 생성 2. PC 메뉴에서 주요 메뉴만 드래그 앤 드롭으로 복사 3. 불필요한 하위 메뉴 정리 4. 모바일 레이아웃에 "모바일 메뉴" 사이트맵 연결 ``` **소요 시간: 5분** ### 시나리오 3: 시즌별 메뉴 교체 **상황** - 이벤트 기간에만 보이는 메뉴 - 평상시에는 숨김 처리 **MenuPro 활용** ``` [평상시] 메인 메뉴 ├ 홈 ├ 소개 └ 게시판 [이벤트 사이트맵] ← 접어두기 ├ 이벤트 안내 ├ 참여 방법 └ 당첨자 발표 [이벤트 시작] → 이벤트 메뉴를 메인 메뉴로 드래그 → 한 번에 저장 → 완료! ``` --- ## 설치 및 시작하기 ### 시스템 요구사항 **필수** - Rhymix 2.1.8 이상 - PHP 7.4 이상 - 모던 브라우저 (Chrome, Firefox, Safari, Edge) **권장** - PHP 8.0 이상 - SSD 스토리지 - 메모리 512MB 이상 ### 설치 방법 **1. 모듈 다운로드** ```bash # Git으로 다운로드 git clone https://github.com/eond/menupro.git modules/menupro # 또는 ZIP 파일 다운로드 후 압축 해제 ``` **2. 관리자 페이지 접속** ``` http://yoursite.com/index.php?module=admin&act=dispMenuproAdminConfig ``` **3. 기본 사용법** ``` 1. 기존 사이트맵이 자동으로 로드됨 2. 드래그 앤 드롭으로 메뉴 정리 3. "저장" 버튼 클릭 4. 완료! 🎉 ``` ### 빌드 (개발자용) ```bash # 의존성 설치 npm install # 개발 모드 (Hot Reload) npm run dev:menupro # 프로덕션 빌드 npm run build:menupro ``` --- ## 버전 히스토리 ### v1.0.7 (2025-11-28) - 최신 - ✨ 위젯페이지/문서페이지 mid 필드 표시 및 수정 기능 - 🐛 프론트엔드 모듈 타입 인식 개선 ### v1.0.6 (2025-11-28) - ✨ getMenuDetail에서 enrichMenuItem 호출 추가 - 🐛 MenuGrid에 module_type 전달 ### v1.0.5 (2025-11-28) - 🐛 외부 페이지(OUTSIDE) 생성 시 "새 창에서 열기" 강제 설정 제거 ### v1.0.4 (2025-11-28) - ✨ 위젯페이지 모듈 타입 인식 개선 (enrichMenuItem 함수 추가) ### v1.0.3 (2025-11-27) - 🐛 메뉴 아이템 mid 변경 시 실제 모듈 mid도 함께 변경되도록 수정 ### v1.0.2 (2025-11-27) - 🐛 **중요 버그 수정**: 메뉴 이동 시 하위 메뉴가 함께 이동하지 않는 문제 해결 - ✨ updateChildrenMenuSrlRecursive 재귀 함수 추가 ### v1.0.1 (2025-11-25) - 🐛 skin/mskin 파라미터 이름 충돌 문제 해결 - ✨ 스킨 저장 시 is_skin_fix 설정 추가 - ✨ 사이트 기본 스킨 정보 표시 ### v1.0.0 (2025-11-14) - 초기 릴리즈 - 🎉 MenuPro 첫 출시 - ✨ React 기반 드래그 앤 드롭 - ✨ 사이트맵 간 메뉴 이동 - ✨ 일괄 저장 시스템 - ✨ 메뉴 검색 기능 --- ## 로드맵: 다음 업데이트 계획 ### v1.1.0 (예정) - [ ] 메뉴 복사/붙여넣기 강화 - [ ] 메뉴 템플릿 기능 (자주 쓰는 구조 저장) - [ ] 메뉴 일괄 편집 기능 ### v1.2.0 (예정) - [ ] 권한 설정 UI 개선 - [ ] 메뉴 아이콘 라이브러리 - [ ] 드래그 앤 드롭 애니메이션 개선 ### v2.0.0 (장기) - [ ] 다중 사이트 지원 - [ ] 메뉴 변경 이력 관리 - [ ] 메뉴 롤백 기능 --- ## FAQ ### Q1. 기존 메뉴 데이터가 손실되나요? **A.** 아니요. MenuPro는 Rhymix 기본 메뉴 테이블을 그대로 사용합니다. 데이터 마이그레이션이 필요 없으며, 언제든 기본 메뉴 관리로 돌아갈 수 있습니다. ### Q2. 기존 메뉴 관리와 함께 사용할 수 있나요? **A.** 네, 가능합니다. MenuPro와 기본 메뉴 관리를 혼용해도 문제없습니다. ### Q3. 성능은 어떤가요? 메뉴가 많으면 느리지 않나요? **A.** 메뉴 1000개까지 테스트했으며, 쾌적하게 동작합니다. React의 Virtual DOM과 효율적인 렌더링으로 빠른 성능을 보장합니다. ### Q4. 모바일에서도 사용할 수 있나요? **A.** 현재는 데스크톱 브라우저 최적화되어 있습니다. 모바일 대응은 v1.1.0에서 추가 예정입니다. ### Q5. 라이선스는 어떻게 되나요? **A.** 유료 라이선스입니다. ### Q6. 버그를 발견했어요. 어디에 제보하나요? **A.** GitHub Issues 또는 EOND 포럼(https://eond.com)에 제보해주세요. --- ## 사용자 후기 (테스트 사용자) > "100개 넘는 메뉴를 관리하는 게 이렇게 쉬울 줄 몰랐습니다. 드래그 앤 드롭 하나로 모든 게 해결되네요." > — K사 웹마스터 > "사이트맵 간 이동이 정말 편합니다. 기존엔 일일이 복사했는데, 이제 드래그 한 번이면 끝!" > — 커뮤니티 관리자 L > "검색 기능이 생각보다 훨씬 유용합니다. 메뉴가 많아도 바로 찾을 수 있어요." > — 포털 운영자 P --- ## 결론 MenuPro는 단순한 도구가 아닙니다. **Rhymix 사이트맵 관리의 패러다임을 바꾸는 혁신**입니다. ### MenuPro가 제공하는 가치 ✅ **생산성**: 작업 시간 6배 단축 ✅ **편의성**: 직관적인 드래그 앤 드롭 ✅ **안정성**: 일괄 저장으로 실수 방지 ✅ **확장성**: 대규모 사이트도 문제없음 ✅ **현대성**: 최신 기술 스택 ### 지금 바로 시작하세요 더 이상 번거로운 메뉴 관리로 시간을 낭비하지 마세요. MenuPro로 10분 만에 끝낼 수 있습니다. **문의**: eond@eond.com --- **작성일**: 2025-12-03 **카테고리**: Rhymix, 모듈, 관리도구 **태그**: #MenuPro #Rhymix #사이트맵 #React #드래그앤드롭 #관리도구 **버전**: MenuPro v1.0.7