[HTML/CSS 입문] AI로 실시간 데이터·검색/필터·테이블/페이징 UI 만들기 (GPT vs Cursor vs AntiGravity) | Part 9

지난 시간 우리는 AI를 활용해 대시보드 차트 UI를 만들어 보았습니다.
점점 완성되어가는 요소들에 여러분들도 조금씩 재미를 느끼셨을 것이라 생각합니다.
또한 이제 어떤 AI가 나와 맞는지, 그리고 어떤 상황에서 어떤 AI를 쓰는게 좋은지 충분히 이해 하셨을 것이라고 여겨집니다.

현대 웹 개발에서 실시간 데이터 업데이트와 사용자 친화적인 UI는 필수 요소입니다.
특히 초급 개발자나 비전공자도 AI 기반 도구를 활용하면 비교적 쉽게 실무형 대시보드를 만들 수 있습니다.

이번 글에서는 GPT, Cursor, AntiGravity를 활용한 실시간 데이터 테이블 + 검색, 필터, 페이징 UI 제작 과정을 소개하며, 각 도구별 특징과 장단점을 비교해봅니다.

핵심 요약: AI 도구를 활용하면 초급 개발자도 실무형 UI를 빠르게 구현할 수 있습니다.

GPT vs Cursor vs AntiGravity 를 AI를 활용하여 시각화 한 이미지 자료입니다. 
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
GPT vs Cursor vs AntiGravity 를 AI를 활용하여 시각화 한 이미지 자료입니다.
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

외부링크(GPT / Cursor / AntiGravity)

핵심 요약: GPT, Cursor, AntiGravity는 각각 다른 특성과 활용 방식으로 UI 코딩을 지원합니다.


GPT / Cursor / AntiGravity에 입력할 공통 프롬프트

실무형 UI를 만들 때 세 도구에 공통으로 입력할 프롬프트 예시는 다음과 같습니다.

"HTML/CSS/JS를 이용해 실시간 데이터 업데이트가 가능한 테이블 UI를 만들어줘.
필터, 검색, 페이징, 정렬 기능 포함.
반응형 디자인 적용, 모바일에서도 자연스럽게 보이도록 구현.
완성 후 HTML/CSS/JS 소스코드를 간단하고 쉽게 설명해 줘."

핵심 요약: 공통 프롬프트를 활용하면 AI별 코드 출력 비교가 가능합니다.

GPT에 입력한 프롬프트(무료버전).
이해를 돕기 위해 제공하는 이미지로서 저작권은 블로그 주인 제로에게 있습니다.
GPT에 입력한 프롬프트(무료버전).
이해를 돕기 위해 제공하는 이미지로서 저작권은 블로그 주인 제로에게 있습니다.
Cursor에 입력한 프롬프트(무료버전)
이해를 돕기 위해 제공하는 이미지로서 저작권은 블로그 주인 제로에게 있습니다.
Cursor에 입력한 프롬프트(무료버전)
이해를 돕기 위해 제공하는 이미지로서 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity에 입력한 프롬프트(무료버전)
이해를 돕기 위해 제공하는 이미지로서 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity에 입력한 프롬프트(무료버전)
이해를 돕기 위해 제공하는 이미지로서 저작권은 블로그 주인 제로에게 있습니다.

GPT vs Cursor vs AntiGravity 비교

GPT 생성 HTML 코드

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>실시간 데이터 테이블</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h2>실시간 데이터 테이블</h2>

    <!-- 검색 및 필터 -->
    <input type="text" id="search" placeholder="검색어 입력..." />

    <!-- 테이블 -->
    <table id="dataTable">
      <thead>
        <tr>
          <th data-column="id" data-order="desc">ID ▲</th>
          <th data-column="name" data-order="desc">이름 ▲</th>
          <th data-column="age" data-order="desc">나이 ▲</th>
          <th data-column="city" data-order="desc">도시 ▲</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>

    <!-- 페이징 -->
    <div id="pagination"></div>
  </div>

  <script src="script.js"></script>
</body>
</html>

GPT 생성 HTML 코드 해설

  • 검색 input과 테이블 구조, 페이징 버튼 영역을 만듦.
  • data-columndata-order 속성으로 정렬 기능 연결.

GPT 생성 CSS 코드

* {
  box-sizing: border-box;
}

body {
  font-family: Arial, sans-serif;
  padding: 20px;
  margin: 0;
}

.container {
  max-width: 1000px;
  margin: 0 auto;
}

h2 {
  text-align: center;
}

#search {
  width: 100%;
  padding: 8px;
  margin-bottom: 10px;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 10px;
}

th, td {
  padding: 8px;
  text-align: left;
  border: 1px solid #ccc;
}

th {
  cursor: pointer;
  background-color: #f2f2f2;
}

#pagination {
  text-align: center;
}

#pagination button {
  padding: 5px 10px;
  margin: 2px;
  cursor: pointer;
}

@media (max-width: 600px) {
  th, td {
    font-size: 12px;
    padding: 6px;
  }

  #search {
    padding: 6px;
  }
}

GPT 생성 CSS 코드 해설

  • 반응형 디자인 적용: 모바일에서는 글자 크기와 padding 축소.
  • 테이블, 버튼 스타일 간단하게 구성.

GPT 생성 Js 코드

// 샘플 데이터
let data = [
  {id: 1, name: "홍길동", age: 25, city: "서울"},
  {id: 2, name: "김철수", age: 30, city: "부산"},
  {id: 3, name: "이영희", age: 28, city: "대구"},
  {id: 4, name: "박민수", age: 35, city: "광주"},
  {id: 5, name: "최수진", age: 22, city: "인천"},
];

// 설정
const rowsPerPage = 3;
let currentPage = 1;
let filteredData = [...data];

const tableBody = document.querySelector("#dataTable tbody");
const searchInput = document.querySelector("#search");
const pagination = document.getElementById("pagination");

// 테이블 렌더링
function renderTable() {
  tableBody.innerHTML = "";

  let start = (currentPage - 1) * rowsPerPage;
  let end = start + rowsPerPage;
  let pageData = filteredData.slice(start, end);

  pageData.forEach(row => {
    let tr = document.createElement("tr");
    tr.innerHTML = `<td>${row.id}</td>
                    <td>${row.name}</td>
                    <td>${row.age}</td>
                    <td>${row.city}</td>`;
    tableBody.appendChild(tr);
  });

  renderPagination();
}

// 페이징 렌더링
function renderPagination() {
  pagination.innerHTML = "";
  let pageCount = Math.ceil(filteredData.length / rowsPerPage);

  for(let i=1; i<=pageCount; i++) {
    let btn = document.createElement("button");
    btn.textContent = i;
    btn.disabled = (i === currentPage);
    btn.addEventListener("click", () => {
      currentPage = i;
      renderTable();
    });
    pagination.appendChild(btn);
  }
}

// 검색 필터
searchInput.addEventListener("input", () => {
  let query = searchInput.value.toLowerCase();
  filteredData = data.filter(item =>
    item.name.toLowerCase().includes(query) ||
    item.city.toLowerCase().includes(query)
  );
  currentPage = 1;
  renderTable();
});

// 정렬
document.querySelectorAll("#dataTable th").forEach(th => {
  th.addEventListener("click", () => {
    const column = th.dataset.column;
    const order = th.dataset.order;

    filteredData.sort((a,b) => {
      if(a[column] < b[column]) return order === "asc" ? -1 : 1;
      if(a[column] > b[column]) return order === "asc" ? 1 : -1;
      return 0;
    });

    th.dataset.order = order === "asc" ? "desc" : "asc";
    th.textContent = th.textContent.slice(0, -1) + (order === "asc" ? "▲" : "▼");
    renderTable();
  });
});

// 실시간 데이터 추가 (5초마다)
setInterval(() => {
  const newId = data.length + 1;
  const cities = ["서울", "부산", "대구", "광주", "인천"];
  const names = ["김", "이", "박", "최", "정"];
  const randomName = names[Math.floor(Math.random()*names.length)] + "새" + newId;
  const randomCity = cities[Math.floor(Math.random()*cities.length)];
  const randomAge = Math.floor(Math.random()*20) + 20;

  data.push({id:newId, name: randomName, age: randomAge, city: randomCity});
  filteredData = [...data];
  renderTable();
}, 5000);

// 초기 렌더링
renderTable();

GPT 생성 Js 코드 해설

  • 데이터 관리: 배열 data에 테이블 데이터 저장.
  • 렌더링: renderTable()로 현재 페이지 데이터를 테이블에 표시.
  • 검색: input 이벤트로 filteredData 갱신 후 테이블 다시 렌더링.
  • 정렬: th 클릭 시 해당 컬럼 기준으로 정렬, ▲▼ 표시 변경.
  • 페이징: 현재 페이지 기준으로 데이터 슬라이스 후 버튼 생성.
  • 실시간 업데이트: 5초마다 랜덤 데이터 생성 후 테이블 갱신.

GPT 코드 해설

  • renderTable 함수로 페이지 단위 데이터 출력
  • 검색, 필터는 추후 JS 이벤트로 확장 가능
  • HTML/CSS/JS 구조가 간단하여 초급자가 수정 가능

핵심 요약: GPT는 이해하기 쉬운 코드와 기본 기능 구현에 강점이 있습니다.

GPT 결과물(스크린샷 위치 지정)

GPT가 생성한 페이지.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
GPT가 생성한 페이지.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
GPT가 생성한 페이지 모바일버전.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
GPT가 생성한 페이지 모바일버전.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.

GPT 장점

  • 코드 가독성 우수
  • 초급자 친화적
  • 기본 기능 구현 빠름

GPT 단점

  • 복잡한 로직 구현에는 코드 추가 필요
  • UI 커스터마이징 제한적

GPT 총평

초급자나 비개발자가 실무형 테이블 UI를 빠르게 구현할 때 적합합니다.


Cursor 코드

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>실시간 데이터 테이블</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
            min-height: 100vh;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
            padding: 30px;
        }

        h1 {
            color: #333;
            margin-bottom: 10px;
            font-size: 28px;
        }

        .subtitle {
            color: #666;
            margin-bottom: 30px;
            font-size: 14px;
        }

        .controls {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin-bottom: 25px;
            align-items: center;
        }

        .search-box {
            flex: 1;
            min-width: 200px;
            position: relative;
        }

        .search-box input {
            width: 100%;
            padding: 12px 40px 12px 15px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.3s;
        }

        .search-box input:focus {
            outline: none;
            border-color: #667eea;
        }

        .search-icon {
            position: absolute;
            right: 15px;
            top: 50%;
            transform: translateY(-50%);
            color: #999;
        }

        .filter-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }

        .filter-select {
            padding: 12px 15px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            background: white;
            cursor: pointer;
            transition: border-color 0.3s;
        }

        .filter-select:focus {
            outline: none;
            border-color: #667eea;
        }

        .status-badge {
            display: inline-block;
            padding: 4px 12px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 600;
        }

        .status-active {
            background: #d4edda;
            color: #155724;
        }

        .status-inactive {
            background: #f8d7da;
            color: #721c24;
        }

        .status-pending {
            background: #fff3cd;
            color: #856404;
        }

        .table-wrapper {
            overflow-x: auto;
            margin-bottom: 20px;
            border-radius: 8px;
            border: 1px solid #e0e0e0;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            background: white;
        }

        thead {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }

        th {
            padding: 15px;
            text-align: left;
            font-weight: 600;
            font-size: 14px;
            cursor: pointer;
            user-select: none;
            position: relative;
        }

        th:hover {
            background: rgba(255, 255, 255, 0.1);
        }

        th.sortable::after {
            content: ' ↕';
            opacity: 0.5;
            font-size: 12px;
        }

        th.sort-asc::after {
            content: ' ↑';
            opacity: 1;
        }

        th.sort-desc::after {
            content: ' ↓';
            opacity: 1;
        }

        td {
            padding: 15px;
            border-bottom: 1px solid #f0f0f0;
            font-size: 14px;
        }

        tbody tr {
            transition: background-color 0.2s;
        }

        tbody tr:hover {
            background-color: #f8f9fa;
        }

        tbody tr:last-child td {
            border-bottom: none;
        }

        .pagination {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 10px;
            flex-wrap: wrap;
            margin-top: 20px;
        }

        .pagination button {
            padding: 10px 15px;
            border: 2px solid #e0e0e0;
            background: white;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
        }

        .pagination button:hover:not(:disabled) {
            background: #667eea;
            color: white;
            border-color: #667eea;
        }

        .pagination button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .pagination .page-info {
            padding: 10px 15px;
            color: #666;
            font-size: 14px;
        }

        .pagination .page-number {
            min-width: 40px;
        }

        .pagination .page-number.active {
            background: #667eea;
            color: white;
            border-color: #667eea;
        }

        .update-indicator {
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: #28a745;
            margin-right: 8px;
            animation: pulse 2s infinite;
        }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        .stats {
            display: flex;
            gap: 20px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }

        .stat-card {
            flex: 1;
            min-width: 150px;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 8px;
            text-align: center;
        }

        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: #667eea;
        }

        .stat-label {
            font-size: 12px;
            color: #666;
            margin-top: 5px;
        }

        /* 모바일 반응형 */
        @media (max-width: 768px) {
            .container {
                padding: 15px;
            }

            h1 {
                font-size: 22px;
            }

            .controls {
                flex-direction: column;
            }

            .search-box {
                width: 100%;
            }

            .filter-group {
                width: 100%;
            }

            .filter-select {
                flex: 1;
            }

            table {
                font-size: 12px;
            }

            th, td {
                padding: 10px 8px;
            }

            .table-wrapper {
                overflow-x: scroll;
                -webkit-overflow-scrolling: touch;
            }

            .pagination {
                gap: 5px;
            }

            .pagination button {
                padding: 8px 12px;
                font-size: 12px;
            }

            .stats {
                flex-direction: column;
            }

            .stat-card {
                width: 100%;
            }
        }

        @media (max-width: 480px) {
            body {
                padding: 10px;
            }

            th, td {
                padding: 8px 5px;
                font-size: 11px;
            }

            .status-badge {
                font-size: 10px;
                padding: 3px 8px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>
            <span class="update-indicator"></span>
            실시간 데이터 테이블
        </h1>
        <p class="subtitle">필터, 검색, 정렬, 페이징 기능이 포함된 반응형 테이블</p>

        <div class="stats">
            <div class="stat-card">
                <div class="stat-value" id="totalCount">0</div>
                <div class="stat-label">전체 데이터</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="filteredCount">0</div>
                <div class="stat-label">필터링된 데이터</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="currentPage">1</div>
                <div class="stat-label">현재 페이지</div>
            </div>
        </div>

        <div class="controls">
            <div class="search-box">
                <input type="text" id="searchInput" placeholder="이름, 이메일, 부서로 검색...">
                <span class="search-icon">🔍</span>
            </div>
            <div class="filter-group">
                <select id="statusFilter" class="filter-select">
                    <option value="">전체 상태</option>
                    <option value="active">활성</option>
                    <option value="inactive">비활성</option>
                    <option value="pending">대기</option>
                </select>
                <select id="departmentFilter" class="filter-select">
                    <option value="">전체 부서</option>
                    <option value="개발팀">개발팀</option>
                    <option value="디자인팀">디자인팀</option>
                    <option value="마케팅팀">마케팅팀</option>
                    <option value="영업팀">영업팀</option>
                </select>
            </div>
        </div>

        <div class="table-wrapper">
            <table id="dataTable">
                <thead>
                    <tr>
                        <th class="sortable" data-sort="id">ID</th>
                        <th class="sortable" data-sort="name">이름</th>
                        <th class="sortable" data-sort="email">이메일</th>
                        <th class="sortable" data-sort="department">부서</th>
                        <th class="sortable" data-sort="status">상태</th>
                        <th class="sortable" data-sort="salary">급여</th>
                        <th class="sortable" data-sort="lastUpdate">마지막 업데이트</th>
                    </tr>
                </thead>
                <tbody id="tableBody">
                    <!-- 데이터가 여기에 동적으로 추가됩니다 -->
                </tbody>
            </table>
        </div>

        <div class="pagination" id="pagination">
            <!-- 페이징 버튼이 여기에 동적으로 추가됩니다 -->
        </div>
    </div>

    <script>
        // 샘플 데이터 생성 함수
        function generateSampleData(count = 50) {
            const names = ['김철수', '이영희', '박민수', '정수진', '최동현', '강미영', '윤성호', '임지은', '한대현', '오수빈'];
            const departments = ['개발팀', '디자인팀', '마케팅팀', '영업팀'];
            const statuses = ['active', 'inactive', 'pending'];
            const data = [];

            for (let i = 1; i <= count; i++) {
                const name = names[Math.floor(Math.random() * names.length)];
                const department = departments[Math.floor(Math.random() * departments.length)];
                const status = statuses[Math.floor(Math.random() * statuses.length)];
                const salary = Math.floor(Math.random() * 5000 + 3000) * 10000;
                
                data.push({
                    id: i,
                    name: name + (i > 10 ? i : ''),
                    email: `user${i}@example.com`,
                    department: department,
                    status: status,
                    salary: salary,
                    lastUpdate: new Date(Date.now() - Math.random() * 86400000 * 7).toLocaleString('ko-KR')
                });
            }

            return data;
        }

        // 전역 변수
        let allData = [];
        let filteredData = [];
        let currentPage = 1;
        const itemsPerPage = 10;
        let sortColumn = 'id';
        let sortDirection = 'asc';

        // 초기 데이터 로드
        function initData() {
            allData = generateSampleData(50);
            filteredData = [...allData];
            renderTable();
            renderPagination();
            updateStats();
        }

        // 검색 및 필터링
        function filterData() {
            const searchTerm = document.getElementById('searchInput').value.toLowerCase();
            const statusFilter = document.getElementById('statusFilter').value;
            const departmentFilter = document.getElementById('departmentFilter').value;

            filteredData = allData.filter(item => {
                const matchesSearch = !searchTerm || 
                    item.name.toLowerCase().includes(searchTerm) ||
                    item.email.toLowerCase().includes(searchTerm) ||
                    item.department.toLowerCase().includes(searchTerm);
                
                const matchesStatus = !statusFilter || item.status === statusFilter;
                const matchesDepartment = !departmentFilter || item.department === departmentFilter;

                return matchesSearch && matchesStatus && matchesDepartment;
            });

            // 정렬 적용
            sortData();
            currentPage = 1;
            renderTable();
            renderPagination();
            updateStats();
        }

        // 정렬 함수
        function sortData() {
            filteredData.sort((a, b) => {
                let aVal = a[sortColumn];
                let bVal = b[sortColumn];

                if (sortColumn === 'salary') {
                    aVal = Number(aVal);
                    bVal = Number(bVal);
                } else if (sortColumn === 'lastUpdate') {
                    aVal = new Date(aVal);
                    bVal = new Date(bVal);
                } else {
                    aVal = String(aVal).toLowerCase();
                    bVal = String(bVal).toLowerCase();
                }

                if (sortDirection === 'asc') {
                    return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
                } else {
                    return aVal < bVal ? 1 : aVal > bVal ? -1 : 0;
                }
            });
        }

        // 테이블 렌더링
        function renderTable() {
            const tbody = document.getElementById('tableBody');
            const startIndex = (currentPage - 1) * itemsPerPage;
            const endIndex = startIndex + itemsPerPage;
            const pageData = filteredData.slice(startIndex, endIndex);

            tbody.innerHTML = '';

            if (pageData.length === 0) {
                tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 40px; color: #999;">데이터가 없습니다.</td></tr>';
                return;
            }

            pageData.forEach(item => {
                const row = document.createElement('tr');
                
                const statusClass = {
                    'active': 'status-active',
                    'inactive': 'status-inactive',
                    'pending': 'status-pending'
                }[item.status] || '';

                const statusText = {
                    'active': '활성',
                    'inactive': '비활성',
                    'pending': '대기'
                }[item.status] || item.status;

                row.innerHTML = `
                    <td>${item.id}</td>
                    <td>${item.name}</td>
                    <td>${item.email}</td>
                    <td>${item.department}</td>
                    <td><span class="status-badge ${statusClass}">${statusText}</span></td>
                    <td>${item.salary.toLocaleString('ko-KR')}원</td>
                    <td>${item.lastUpdate}</td>
                `;
                
                tbody.appendChild(row);
            });

            // 정렬 헤더 업데이트
            updateSortHeaders();
        }

        // 정렬 헤더 업데이트
        function updateSortHeaders() {
            document.querySelectorAll('th.sortable').forEach(th => {
                th.classList.remove('sort-asc', 'sort-desc');
                if (th.dataset.sort === sortColumn) {
                    th.classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
                }
            });
        }

        // 페이징 렌더링
        function renderPagination() {
            const pagination = document.getElementById('pagination');
            const totalPages = Math.ceil(filteredData.length / itemsPerPage);

            if (totalPages <= 1) {
                pagination.innerHTML = '';
                return;
            }

            let html = '';

            // 이전 버튼
            html += `<button onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>이전</button>`;

            // 페이지 번호
            const maxVisiblePages = 5;
            let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
            let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);

            if (endPage - startPage < maxVisiblePages - 1) {
                startPage = Math.max(1, endPage - maxVisiblePages + 1);
            }

            if (startPage > 1) {
                html += `<button class="page-number" onclick="changePage(1)">1</button>`;
                if (startPage > 2) {
                    html += `<span class="page-info">...</span>`;
                }
            }

            for (let i = startPage; i <= endPage; i++) {
                html += `<button class="page-number ${i === currentPage ? 'active' : ''}" onclick="changePage(${i})">${i}</button>`;
            }

            if (endPage < totalPages) {
                if (endPage < totalPages - 1) {
                    html += `<span class="page-info">...</span>`;
                }
                html += `<button class="page-number" onclick="changePage(${totalPages})">${totalPages}</button>`;
            }

            // 다음 버튼
            html += `<button onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>다음</button>`;

            pagination.innerHTML = html;
        }

        // 페이지 변경
        function changePage(page) {
            const totalPages = Math.ceil(filteredData.length / itemsPerPage);
            if (page < 1 || page > totalPages) return;
            
            currentPage = page;
            renderTable();
            renderPagination();
            updateStats();
            
            // 페이지 상단으로 스크롤
            document.querySelector('.table-wrapper').scrollIntoView({ behavior: 'smooth', block: 'start' });
        }

        // 통계 업데이트
        function updateStats() {
            document.getElementById('totalCount').textContent = allData.length;
            document.getElementById('filteredCount').textContent = filteredData.length;
            document.getElementById('currentPage').textContent = currentPage;
        }

        // 정렬 처리
        function handleSort(column) {
            if (sortColumn === column) {
                sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
            } else {
                sortColumn = column;
                sortDirection = 'asc';
            }
            
            sortData();
            currentPage = 1;
            renderTable();
            renderPagination();
        }

        // 실시간 데이터 업데이트 (일부 데이터만 랜덤하게 변경)
        function updateDataRealtime() {
            // 랜덤하게 몇 개의 데이터를 업데이트
            const updateCount = Math.floor(Math.random() * 3) + 1; // 1-3개
            
            for (let i = 0; i < updateCount; i++) {
                const randomIndex = Math.floor(Math.random() * allData.length);
                const item = allData[randomIndex];
                
                // 상태나 급여를 랜덤하게 변경
                if (Math.random() > 0.5) {
                    const statuses = ['active', 'inactive', 'pending'];
                    item.status = statuses[Math.floor(Math.random() * statuses.length)];
                } else {
                    const change = Math.floor(Math.random() * 1000000) - 500000;
                    item.salary = Math.max(30000000, item.salary + change);
                }
                
                item.lastUpdate = new Date().toLocaleString('ko-KR');
            }
            
            // 필터링 및 렌더링 다시 수행
            filterData();
        }

        // 이벤트 리스너 설정
        document.addEventListener('DOMContentLoaded', function() {
            initData();

            // 검색 입력
            document.getElementById('searchInput').addEventListener('input', filterData);

            // 필터 변경
            document.getElementById('statusFilter').addEventListener('change', filterData);
            document.getElementById('departmentFilter').addEventListener('change', filterData);

            // 정렬 헤더 클릭
            document.querySelectorAll('th.sortable').forEach(th => {
                th.addEventListener('click', () => handleSort(th.dataset.sort));
            });

            // 실시간 업데이트 (5초마다)
            setInterval(updateDataRealtime, 5000);
        });
    </script>
</body>
</html>

Cursor 코드 해설

  • HTML 구조
    • 통계 카드: 전체 데이터 수, 필터링된 데이터 수, 현재 페이지 표시
    • 컨트롤 영역: 검색 입력창과 필터 드롭다운
    • 테이블: 7개 컬럼(ID, 이름, 이메일, 부서, 상태, 급여, 마지막 업데이트)
    • 페이징: 이전/다음 버튼과 페이지 번호
  • CSS 스타일링
    • 그라데이션 배경과 카드 디자인
    • 호버 효과와 트랜지션
    • 정렬 아이콘(↑↓) 표시
    • 상태 배지 색상 구분(활성=초록, 비활성=빨강, 대기=노랑)
    • 반응형 미디어 쿼리:
      • 768px 이하: 모바일 레이아웃
      • 480px 이하: 더 작은 폰트와 패딩
  • JavaScript 기능
    • 데이터 생성 (generateSampleData)
      • 50개의 샘플 데이터 자동 생성
    • 필터링 (filterData)
      • 검색어와 필터 조건을 조합해 데이터 필터링
    • 정렬 (sortData, handleSort)
      • 컬럼 클릭 시 정렬 방향 토글
      • 숫자, 날짜, 문자열 자동 처리
    • 페이징 (renderPagination, changePage)
      • 페이지당 10개 항목 표시
      • 최대 5개 페이지 번호 표시
  • 실시간 업데이트 (updateDataRealtime)
    • 5초마다 랜덤 데이터 변경
    • 상태나 급여 값 업데이트

Cursor 결과물(스크린샷 위치 지정)

Cursor가 생성한 페이지
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
Cursor가 생성한 페이지
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
Cursor가 생성한 페이지 모바일 버전.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
Cursor가 생성한 페이지 모바일 버전.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.

핵심 요약: Cursor는 모듈 기반으로 실무형 UI를 쉽게 확장할 수 있습니다.

Cursor 장점

  • 모듈화된 코드 제공
  • 확장성과 유지보수 용이

Cursor 단점

  • 외부 라이브러리 의존성 있음
  • 초급자에게는 다소 복잡할 수 있음

Cursor 총평

중급 이상 개발자나 확장 가능한 UI 제작에 적합합니다.


AntiGravity 가 생성한 HTML 코드

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-time Data Dashboard</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="app-container">
        <header class="app-header">
            <div>
                <h1>Data Monitor</h1>
                <p>Live system status and transaction tracking</p>
            </div>
            <div class="live-indicator">
                <span class="pulse"></span> Live Updates On
            </div>
        </header>

        <main class="data-panel">
            <div class="controls-bar">
                <div class="search-group">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
                        stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <circle cx="11" cy="11" r="8"></circle>
                        <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
                    </svg>
                    <input type="text" id="searchInput" placeholder="Search by name...">
                </div>

                <div class="filters-group">
                    <select id="categoryFilter">
                        <option value="">All Categories</option>
                        <option value="Server">Server</option>
                        <option value="Database">Database</option>
                        <option value="Network">Network</option>
                        <option value="Application">Application</option>
                    </select>

                    <select id="statusFilter">
                        <option value="">All Status</option>
                        <option value="Active">Active</option>
                        <option value="Warning">Warning</option>
                        <option value="Critical">Critical</option>
                        <option value="Offline">Offline</option>
                    </select>
                </div>
            </div>

            <div class="table-container">
                <table id="dataTable">
                    <thead>
                        <tr>
                            <th data-sort="id">ID <span class="sort-icon">↕</span></th>
                            <th data-sort="name">Name <span class="sort-icon">↕</span></th>
                            <th data-sort="category">Category <span class="sort-icon">↕</span></th>
                            <th data-sort="status">Status <span class="sort-icon">↕</span></th>
                            <th data-sort="value">Load (%) <span class="sort-icon">↕</span></th>
                            <th data-sort="timestamp">Last Updated <span class="sort-icon">↕</span></th>
                        </tr>
                    </thead>
                    <tbody id="tableBody">
                        <!-- Data will be populated here -->
                    </tbody>
                </table>
                <div id="noResults" class="no-results hidden">
                    No results found matching your criteria.
                </div>
            </div>

            <div class="pagination-bar">
                <div class="rows-per-page">
                    <label>Rows per page:</label>
                    <select id="rowsPerPage">
                        <option value="5">5</option>
                        <option value="10" selected>10</option>
                        <option value="20">20</option>
                        <option value="50">50</option>
                    </select>
                </div>
                <div class="pagination-controls">
                    <span id="pageInfo">Page 1 of 1</span>
                    <div class="page-buttons">
                        <button id="prevPage" disabled>Previous</button>
                        <button id="nextPage" disabled>Next</button>
                    </div>
                </div>
            </div>
        </main>
    </div>
    <script src="script.js"></script>
</body>

</html>

AntiGravity 가 생성한 HTML 코드 설명

웹페이지의 뼈대를 담당합니다.

  • 헤더 (header): 제목과 “Live Updates On” 표시등을 배치했습니다.
  • 컨트롤 패널 (.controls-bar): 검색창(input), 카테고리/상태 필터(select)를 한곳에 모았습니다.
  • 테이블 (table): 데이터를 보여줄 표입니다. thead는 제목줄, tbody는 실제 데이터가 들어갈 공간입니다.
  • 페이지네이션 (.pagination-bar): 페이지 이동 버튼과 보기 설정을 하단에 배치했습니다.

AntiGravity 가 생성한 CSS 코드

:root {
    --bg-color: #f8fafc;
    --surface-color: #ffffff;
    --primary-color: #3b82f6;
    --text-primary: #1e293b;
    --text-secondary: #64748b;
    --border-color: #e2e8f0;
    --hover-bg: #f1f5f9;
    
    --status-active-bg: #dcfce7;
    --status-active-text: #166534;
    --status-warning-bg: #fef9c3;
    --status-warning-text: #854d0e;
    --status-critical-bg: #fee2e2;
    --status-critical-text: #991b1b;
    --status-offline-bg: #f1f5f9;
    --status-offline-text: #475569;

    --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Inter', sans-serif;
    background-color: var(--bg-color);
    color: var(--text-primary);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

.app-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

/* Header */
.app-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 2rem;
}

.app-header h1 {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.025em;
}

.app-header p {
    color: var(--text-secondary);
    font-size: 0.875rem;
}

.live-indicator {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--status-active-text);
    background: var(--status-active-bg);
    padding: 0.25rem 0.75rem;
    border-radius: 9999px;
}

.pulse {
    width: 8px;
    height: 8px;
    background-color: #22c55e;
    border-radius: 50%;
    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .5; }
}

/* Data Panel */
.data-panel {
    background: var(--surface-color);
    border-radius: 1rem;
    box-shadow: var(--shadow-md);
    border: 1px solid var(--border-color);
    overflow: hidden;
}

/* Controls */
.controls-bar {
    padding: 1.5rem;
    border-bottom: 1px solid var(--border-color);
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    justify-content: space-between;
}

.search-group {
    position: relative;
    flex: 1;
    min-width: 250px;
}

.search-group svg {
    position: absolute;
    left: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-secondary);
}

.search-group input {
    width: 100%;
    padding: 0.625rem 1rem 0.625rem 2.5rem;
    border: 1px solid var(--border-color);
    border-radius: 0.5rem;
    font-size: 0.875rem;
    outline: none;
    transition: all 0.2s;
}

.search-group input:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.filters-group {
    display: flex;
    gap: 0.5rem;
}

select {
    padding: 0.625rem 2rem 0.625rem 1rem;
    border: 1px solid var(--border-color);
    border-radius: 0.5rem;
    font-size: 0.875rem;
    background-color: var(--surface-color);
    cursor: pointer;
    outline: none;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
    background-position: right 0.5rem center;
    background-repeat: no-repeat;
    background-size: 1.5em 1.5em;
    appearance: none;
}

select:focus {
    border-color: var(--primary-color);
}

/* Table */
.table-container {
    overflow-x: auto;
    width: 100%;
}

table {
    width: 100%;
    border-collapse: collapse;
    text-align: left;
}

th {
    padding: 1rem 1.5rem;
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-secondary);
    border-bottom: 1px solid var(--border-color);
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}

th:hover {
    background-color: var(--bg-color);
}

td {
    padding: 1rem 1.5rem;
    border-bottom: 1px solid var(--border-color);
    font-size: 0.875rem;
    color: var(--text-primary);
}

tr:hover td {
    background-color: var(--hover-bg);
}

.sort-icon {
    display: inline-block;
    margin-left: 0.25rem;
    opacity: 0.3;
}

th.sort-asc .sort-icon {
    content: "▲";
    opacity: 1;
}

th.sort-desc .sort-icon {
    content: "▼";
    opacity: 1;
}

/* Status Badges */
.status-badge {
    padding: 0.25rem 0.75rem;
    border-radius: 9999px;
    font-size: 0.75rem;
    font-weight: 500;
    display: inline-block;
}

.status-Active { background: var(--status-active-bg); color: var(--status-active-text); }
.status-Warning { background: var(--status-warning-bg); color: var(--status-warning-text); }
.status-Critical { background: var(--status-critical-bg); color: var(--status-critical-text); }
.status-Offline { background: var(--status-offline-bg); color: var(--status-offline-text); }

/* Pagination */
.pagination-bar {
    padding: 1rem 1.5rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-top: 1px solid var(--border-color);
    background-color: var(--bg-color);
}

.page-buttons {
    display: flex;
    gap: 0.5rem;
}

button {
    padding: 0.5rem 1rem;
    border: 1px solid var(--border-color);
    background: var(--surface-color);
    border-radius: 0.375rem;
    cursor: pointer;
    font-size: 0.875rem;
    color: var(--text-primary);
    transition: all 0.2s;
}

button:hover:not(:disabled) {
    border-color: var(--primary-color);
    color: var(--primary-color);
}

button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background-color: var(--bg-color);
}

.hidden {
    display: none;
}

.no-results {
    padding: 3rem;
    text-align: center;
    color: var(--text-secondary);
}

/* Responsive - Mobile Card View */
@media (max-width: 768px) {
    .app-container {
        padding: 1rem;
    }

    .controls-bar {
        padding: 1rem;
        flex-direction: column;
    }

    .filters-group {
        flex-direction: row; /* keep filters side by side if possible */
        width: 100%;
    }
    
    .filters-group select {
        flex: 1;
    }

    /* Hide table header on mobile */
    thead {
        display: none;
    }

    tr {
        display: block;
        margin-bottom: 1rem;
        background: var(--surface-color);
        border: 1px solid var(--border-color);
        border-radius: 0.75rem;
        padding: 1rem;
    }

    td {
        display: flex;
        justify-content: space-between;
        padding: 0.5rem 0;
        border-bottom: none;
        align-items: center;
    }

    td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--text-secondary);
        font-size: 0.875rem;
    }

    .pagination-bar {
        flex-direction: column;
        gap: 1rem;
    }
}

AntiGravity 가 생성한 CSS 코드 해설

화면을 예쁘고 보기 좋게 만듭니다.

  • 모던 디자인Inter 폰트와 부드러운 그림자, 둥근 모서리를 사용해 깔끔한 느낌을 줬습니다.
  • 상태 배지: ‘Active’, ‘Critical’ 등의 상태에 따라 색상이 자동으로 바뀌도록 클래스(status-Active 등)를 정의했습니다.
  • 반응형 (Mobile): 모바일 화면(768px 이하)에서는 표(Table) 모양을 카드(Card) 형태로 변환하여 작은 화면에서도 데이터를 쉽게 볼 수 있게 만들었습니다.

AntiGravity 가 생성한 Js 코드

// State
let data = [];
const TOTAL_ROWS = 100;
let currentPage = 1;
let rowsPerPage = 10;
let sortCol = 'timestamp';
let sortAsc = false;
let filterText = '';
let filterCategory = '';
let filterStatus = '';

// Constants
const CATEGORIES = ['Server', 'Database', 'Network', 'Application'];
const STATUSES = ['Active', 'Warning', 'Critical', 'Offline'];

// Mock Data Generator
function generateData() {
    const newData = [];
    for (let i = 1; i <= TOTAL_ROWS; i++) {
        newData.push({
            id: `SYS-${String(i).padStart(3, '0')}`,
            name: `Node ${i} - ${Math.random().toString(36).substring(7)}`,
            category: CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)],
            status: STATUSES[Math.floor(Math.random() * STATUSES.length)],
            value: Math.floor(Math.random() * 100),
            timestamp: new Date(Date.now() - Math.floor(Math.random() * 10000000)).toISOString()
        });
    }
    return newData;
}

// Initialization
function init() {
    data = generateData();
    setupEventListeners();
    render();
    startRealTimeUpdates();
}

function setupEventListeners() {
    // Search
    document.getElementById('searchInput').addEventListener('input', (e) => {
        filterText = e.target.value.toLowerCase();
        currentPage = 1; // Reset to first page
        render();
    });

    // Filters
    document.getElementById('categoryFilter').addEventListener('change', (e) => {
        filterCategory = e.target.value;
        currentPage = 1;
        render();
    });

    document.getElementById('statusFilter').addEventListener('change', (e) => {
        filterStatus = e.target.value;
        currentPage = 1;
        render();
    });

    // Sort
    document.querySelectorAll('th[data-sort]').forEach(th => {
        th.addEventListener('click', () => {
            const col = th.dataset.sort;
            if (sortCol === col) {
                sortAsc = !sortAsc;
            } else {
                sortCol = col;
                sortAsc = true;
            }
            render();
        });
    });

    // Pagination
    document.getElementById('rowsPerPage').addEventListener('change', (e) => {
        rowsPerPage = parseInt(e.target.value);
        currentPage = 1;
        render();
    });

    document.getElementById('prevPage').addEventListener('click', () => {
        if (currentPage > 1) {
            currentPage--;
            render();
        }
    });

    document.getElementById('nextPage').addEventListener('click', () => {
        const totalPages = Math.ceil(getFilteredData().length / rowsPerPage);
        if (currentPage < totalPages) {
            currentPage++;
            render();
        }
    });
}

// Core Logic
function getFilteredData() {
    return data.filter(row => {
        const matchesText = row.name.toLowerCase().includes(filterText) ||
            row.id.toLowerCase().includes(filterText);
        const matchesCategory = filterCategory === '' || row.category === filterCategory;
        const matchesStatus = filterStatus === '' || row.status === filterStatus;
        return matchesText && matchesCategory && matchesStatus;
    });
}

function getSortedData(filteredData) {
    return [...filteredData].sort((a, b) => {
        let valA = a[sortCol];
        let valB = b[sortCol];

        if (sortCol === 'value') {
            valA = Number(valA);
            valB = Number(valB);
        }

        if (valA < valB) return sortAsc ? -1 : 1;
        if (valA > valB) return sortAsc ? 1 : -1;
        return 0;
    });
}

function render() {
    const filtered = getFilteredData();
    const sorted = getSortedData(filtered);

    // Pagination Calculation
    const totalItems = sorted.length;
    const totalPages = Math.ceil(totalItems / rowsPerPage) || 1;

    // Ensure current page is valid
    if (currentPage > totalPages) currentPage = totalPages;

    const start = (currentPage - 1) * rowsPerPage;
    const end = start + rowsPerPage;
    const pageData = sorted.slice(start, end);

    renderTable(pageData);
    renderPagination(totalItems, totalPages);
    updateSortIcons();

    const noResults = document.getElementById('noResults');
    if (totalItems === 0) {
        noResults.classList.remove('hidden');
    } else {
        noResults.classList.add('hidden');
    }
}

function renderTable(rows) {
    const tbody = document.getElementById('tableBody');
    tbody.innerHTML = rows.map(row => `
        <tr>
            <td data-label="ID">${row.id}</td>
            <td data-label="Name" style="font-weight: 500">${row.name}</td>
            <td data-label="Category">${row.category}</td>
            <td data-label="Status">
                <span class="status-badge status-${row.status}">${row.status}</span>
            </td>
            <td data-label="Load">
                <div style="display: flex; align-items: center; gap: 0.5rem;">
                    <div style="flex: 1; height: 6px; background: #e2e8f0; border-radius: 3px; min-width: 60px;">
                        <div style="width: ${row.value}%; height: 100%; background: ${getColorForValue(row.value)}; border-radius: 3px;"></div>
                    </div>
                    <span>${row.value}%</span>
                </div>
            </td>
            <td data-label="Last Updated">${formatDate(row.timestamp)}</td>
        </tr>
    `).join('');
}

function renderPagination(totalItems, totalPages) {
    document.getElementById('pageInfo').textContent = `Page ${currentPage} of ${totalPages} (${totalItems} items)`;
    document.getElementById('prevPage').disabled = currentPage === 1;
    document.getElementById('nextPage').disabled = currentPage === totalPages || totalPages === 0;
}

function updateSortIcons() {
    document.querySelectorAll('th[data-sort]').forEach(th => {
        th.classList.remove('sort-asc', 'sort-desc');
        th.querySelector('.sort-icon').textContent = '↕';

        if (th.dataset.sort === sortCol) {
            th.classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
            th.querySelector('.sort-icon').textContent = sortAsc ? '▲' : '▼';
        }
    });
}

// Utilities
function getColorForValue(val) {
    if (val < 50) return '#22c55e'; // Green
    if (val < 80) return '#eab308'; // Yellow
    return '#ef4444'; // Red
}

function formatDate(isoString) {
    const d = new Date(isoString);
    return d.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
}

// Real-time Simulation
function startRealTimeUpdates() {
    setInterval(() => {
        // Randomly update 3 rows
        for (let i = 0; i < 3; i++) {
            const idx = Math.floor(Math.random() * data.length);
            data[idx].value = Math.floor(Math.random() * 100);
            data[idx].status = Math.random() > 0.9 ? 'Critical' : (Math.random() > 0.7 ? 'Warning' : 'Active');
            data[idx].timestamp = new Date().toISOString();
        }

        // Randomly add a new row 5% of the time, remove oldest if too many
        if (Math.random() > 0.95) {
            const nextId = data.length + 1;
            const newRow = {
                id: `SYS-${String(nextId).padStart(3, '0')}`,
                name: `New Node ${nextId}`,
                category: CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)],
                status: 'Active',
                value: Math.floor(Math.random() * 50),
                timestamp: new Date().toISOString()
            };
            data.unshift(newRow); // Add to top
            if (data.length > 200) data.pop(); // Keep list manageable
        }

        // Only re-render if the changes affect the current view
        // For simplicity in this demo, we re-render every time
        render();
    }, 2000); // Update every 2 seconds
}

// Start App
init();

AntiGravity 가 생성한 Js 코드 해설

실제 작동하는 로직을 담당합니다.

  • 데이터 생성 (generateData): 100개의 가짜 데이터를 자동으로 만듭니다.
  • 실시간 업데이트 (setInterval)2초마다 무작위로 데이터의 상태나 로드율을 변경하고, 가끔 새로운 데이터를 맨 위에 추가하여 “살아있는” 느낌을 줍니다.
  • 필터링 & 검색 (getFilteredData): 검색어타 필터 조건에 맞는 데이터만 쏙 뽑아냅니다.
  • 정렬 (getSortedData): 컬럼 제목을 클릭하면 오름차순/내림차순으로 정렬해 줍니다.
  • 렌더링 (render): 데이터가 바뀌거나 필터를 건드릴 때마다 화면을 새로 그려서 항상 최신 상태를 보여줍니다.

AntiGravity 결과물(스크린샷 위치 지정)

AntiGravity 가 생성한 페이지.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity 가 생성한 페이지.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity가 생성한 페이지 모바일버전.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity가 생성한 페이지 모바일버전.
이해를 돕기 위해 제공하는 이미지로 저작권은 블로그 주인 제로에게 있습니다.

핵심 요약: AntiGravity는 API 연동과 실시간 데이터 반영에 강점이 있습니다.

AntiGravity 장점

  • 실시간 데이터 처리 우수
  • 반응형 UI 내장

AntiGravity 단점

  • 라이브러리 의존성 있음
  • 초급자 이해 난이도 높음

AntiGravity 총평

실시간 대시보드나 실무형 UI 구현에 적합하지만, 초급자는 학습 필요.


GPT vs Cursor vs AntiGravity 비교총평

이번에는 굉장히 흥미로운 결과가 나왔습니다.
GPT는 역시 가장 기본이 되는 틀. 가장 기본이 되는 페이지를 제작해주며 가장 보편화된 “기본” 에 초점이 맞춰진 것을 알 수 있습니다.
Cursor는 실무 최강인 만큼 바로 사용할 수 있는 페이지를 제작해주었습니다만 이번에는 모든 코드를 하나의 파일로 만들었습니다.
AntiGravity는 역시 우리가 google에서 주로 봐 오던 디자인이었습니다.

부업러, 비개발자 관점

  • GPT: 빠른 구현, 수정 쉬움
  • Cursor: 모듈화, 유지보수 편리
  • AntiGravity: 실시간 데이터, 고급 기능

입문 및 초급 개발자 관점

  • GPT → 빠른 학습용
  • Cursor → 확장성 학습용
  • AntiGravity → 실무형 API 연동 학습용

핵심 요약: 목적과 수준에 따라 AI 도구를 선택하면 효율적입니다.


실무형 UI 구현 팁

AI 개발 입문자 추천 도구 5가지

  1. GPT – 코드 이해와 학습용
  2. Cursor – 확장형 모듈 UI
  3. AntiGravity – 실시간 API 연동
  4. CodeSandbox – 웹 UI 테스트
  5. Figma – UI 시각화

핵심 요약: AI 도구와 실습 환경을 함께 활용하면 빠른 UI 구현 가능.


마무리하며

이번 글에서는 GPT, Cursor, AntiGravity를 활용해 실시간 데이터 UI, 검색/필터, 테이블 페이징 구현법과 각 도구 비교를 다뤘습니다.
개인적으로는 이번에 Cursor의 결과 보다 AntiGravity의 결과가 더 놀라웠습니다. 약간 cursor 가 갤럭시의 느낌이면 antigravity는 아이폰의 느낌이랄까요. 어떤 디자인을 선호하는가에 따라 여러분들도 선택하시면 더 좋은 결과를 보일 수 있을 것 같습니다.

그리고 역시 이번 실습에서도 무지성으로 AI를 사용하는 것이 아닌 서로 어떤 차이가 있는지 비교하고 분석하며 더 나은 효율을 위한 실습을 해보았습니다. 초급 개발자, 비전공자도 AI를 활용하면 실무형 대시보드를 효율적으로 구현할 수 있습니다.


다음 편 예고

다음 편 Part 10에서는 이번 글(Part 9)에서 배운 검색/필터/페이징 UI를 확장하여 실무형 대시보드를 완성합니다.
실시간 데이터 UI차트 연동, KPI 요약 표시 기능을 추가해, 초급 개발자와 비전공자도 바로 적용 가능한 AI코딩 실무 예제를 제공할까 합니다.

특히 GPT, Cursor, AntiGravity를 활용해 각 도구별 차트 연동 방식과 장단점을 비교하고, 실무에서 가장 많이 쓰는 대시보드 구성법까지 심화 학습할 수 있습니다. 다음 편도 많은 기대 부탁드립니다.

핵심 요약: Part 10에서는 Part 9에서 만든 테이블 UI를 실무형 대시보드 완성 단계로 확장합니다.

이전편 다시보기

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤