부동산 데이터 웹앱 만들기 (Apps Script 활용)

소개

부동산 데이터 분석에 관심이 많아서 관련 내용으로 계속 포스팅을 하게 되는 것 같습니다. Apps Script로 무엇을 만들까 생각하다가 부동산 데이터를 제공하는 웹앱이 여럿 눈에 띄는데 Apps Script로 따라서 만들어볼까 싶어서 진행하게 되었습니다. (이미 태블로로 훨씬 더 정교한 분석을 해놓은 상태이긴 하나, Apps Script 공부할겸 진행했습니다.)

◼︎ 벤치마킹 페이지

한국어 텍스트가있는 웹 페이지의 스크린 샷

진행 방법

  • 도구 : Apps Script

  • 모델 : Google AI Studio > Gemini 2.5 Pro Preview O3-25

>> Apps Scipt로 만들고 싶다고 하니 아래처럼 답변해주네요.

공공데이터포털 API를 활용하고 Google Apps Script (GAS)를 사용하여 스크린샷과 유사한 웹앱을 만드는 것은 충분히 가능합니다. Apps Script는 Google 인프라(특히 Google Sheets)와 잘 통합되고 무료로 웹앱을 호스팅할 수 있다는 장점이 있지만, 전문적인 웹 프레임워크(React, Vue 등)에 비해 UI 구현이나 성능 면에서는 제약이 있을 수 있습니다.

아무튼 Gemini가 답변해준 단계별 가이드입니다.

핵심 아이디어:

  1. 데이터 수집 (백엔드 - Apps Script): Apps Script를 이용해 공공데이터포털 API를 주기적으로 호출하여 최신 부동산 데이터를 가져옵니다.

  2. 데이터 저장/캐싱 (백엔드 - Google Sheets): 가져온 데이터를 Google Sheets에 저장하여 웹앱 요청 시 빠르게 데이터를 제공하고 API 호출 횟수를 줄입니다. (선택 사항이지만 권장)

  3. 웹 인터페이스 제공 (프론트엔드 - HTML Service): Apps Script의 HTML Service를 사용하여 사용자가 볼 웹 페이지(HTML, CSS, JavaScript)를 만듭니다.

  4. 데이터 표시 및 상호작용 (프론트엔드/백엔드 연동): 웹 페이지(HTML)에서 Apps Script 함수를 호출하여 Google Sheets에 저장된 데이터를 가져와 테이블 형태로 표시하고, 필터링/정렬 기능을 구현합니다.

개발 단계:

1단계: 프로젝트 설정 및 데이터 시트 준비

  1. Google Sheets 생성: 부동산 데이터를 저장할 새로운 Google Sheet를 만듭니다. 스크린샷에 보이는 컬럼들(단지명, 동, 연식, 세대수, 평형, 면적, 층, 매매가, 전세가, 전세가율 등)을 첫 행에 헤더로 입력합니다.

  2. Apps Script 편집기 열기: 해당 Sheet에서 도구 > 스크립트 편집기를 선택하여 Apps Script 프로젝트를 생성합니다. (스프레드시트에 바인딩된 스크립트)

2단계: 공공데이터 API 호출 및 데이터 저장 (Apps Script - .gs 파일)

  1. API 키 관리: 공공데이터포털에서 발급받은 API 키를 안전하게 관리합니다. 스크립트 속성(파일 > 프로젝트 속성 > 스크립트 속성)에 저장하는 것이 코드에 직접 넣는 것보다 안전합니다.

  2. 데이터 가져오기 함수 작성 (fetchDataFromAPI):

    • UrlFetchApp.fetch(url, options)를 사용하여 공공데이터 API를 호출합니다.

    • API 명세에 따라 필요한 파라미터(서비스 키, 지역 코드, 날짜 등)를 포함하여 URL을 구성합니다.

    • API 응답(주로 XML 또는 JSON)을 파싱합니다. (XmlService 또는 JSON.parse())

    • 파싱된 데이터를 Google Sheets에 저장하기 좋은 형태(예: 2차원 배열)로 가공합니다.

  3. 시트에 데이터 업데이트 함수 작성 (updateSheetData):

    • SpreadsheetApp.getActiveSpreadsheet().getSheetByName("시트이름")으로 시트 객체를 가져옵니다.

    • 기존 데이터를 지웁니다 (sheet.clearContents() 또는 특정 범위 지정).

    • fetchDataFromAPI 함수를 호출하여 새 데이터를 가져옵니다.

    • sheet.getRange(row, col, numRows, numCols).setValues(dataArray)를 사용하여 가공된 데이터를 시트에 씁니다.

  4. 트리거 설정 (자동 업데이트):

    • 스크립트 편집기 왼쪽 메뉴에서 트리거를 선택합니다.

    • + 트리거 추가 버튼을 클릭합니다.

    • 실행할 함수 선택: updateSheetData (또는 데이터 가져오기/업데이트를 총괄하는 함수)

    • 이벤트 소스 선택: 시간 기반

    • 시간 기반 트리거 유형 선택: 일별 타이머 (또는 원하는 주기)

    • 시간 선택: API 데이터 업데이트 주기에 맞춰 설정 (예: 매일 새벽)

    • 저장하고 필요한 권한을 승인합니다.

// 예시: Code.gs (일부)

// 스크립트 속성에서 API 키 가져오기
const API_KEY = PropertiesService.getScriptProperties().getProperty('PUBLIC_DATA_API_KEY');
const SHEET_NAME = "부동산데이터"; // 실제 시트 이름으로 변경

function updateSheetData() {
  const apiUrl = "YOUR_PUBLIC_DATA_API_ENDPOINT"; // 실제 API 엔드포인트
  const params = {
    'serviceKey': API_KEY,
    'pageNo': '1',
    'numOfRows': '1000', // 필요에 따라 조절
    'LAWD_CD': '11680', // 예: 강남구 지역 코드
    'DEAL_YMD': '202401' // 예: 조회 월
    // 기타 필요한 파라미터 추가
  };

  // URL 파라미터 문자열 생성 (직접 만들거나 라이브러리 활용)
  let queryString = Object.keys(params).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key])).join('&');
  let fullUrl = apiUrl + '?' + queryString;

  try {
    const response = UrlFetchApp.fetch(fullUrl);
    const xmlText = response.getContentText(); // 또는 JSON.parse(response.getContentText())
    
    // --- XML 또는 JSON 파싱 로직 ---
    // 예시: XML 파싱 (XmlService 사용)
    const document = XmlService.parse(xmlText);
    const root = document.getRootElement();
    const items = root.getChild('body').getChild('items').getChildren('item');
    
    const dataArray = [['단지명', '거래금액', '전용면적', '거래년도', '거래월', '거래일', '층']]; // 헤더 포함 (필요시)
    
    items.forEach(item => {
      const aptName = item.getChildText('아파트'); // XML 태그명은 실제 API 응답 확인 필요
      const dealAmount = item.getChildText('거래금액').trim(); 
      const area = item.getChildText('전용면적');
      const year = item.getChildText('년');
      const month = item.getChildText('월');
      const day = item.getChildText('일');
      const floor = item.getChildText('층');
      
      // 필요한 데이터 추출 및 배열에 추가
      dataArray.push([aptName, dealAmount, area, year, month, day, floor]); 
    });
    // --- 파싱 로직 끝 ---

    // 시트에 데이터 쓰기
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(SHEET_NAME);
    sheet.clearContents(); // 기존 데이터 삭제 (필요시 범위 지정)
    sheet.getRange(1, 1, dataArray.length, dataArray[0].length).setValues(dataArray);
    Logger.log("데이터 업데이트 완료.");

  } catch (e) {
    Logger.log("API 호출 또는 데이터 처리 오류: " + e);
  }
}

3단계: 웹 인터페이스 생성 (Apps Script - HTML 파일)

  1. HTML 파일 생성: 스크립트 편집기에서 파일 > 새로 만들기 > HTML 파일을 선택하고 이름을 Index.html (또는 원하는 이름)로 지정합니다.

  2. HTML 구조 작성 (Index.html):

    • 표준 HTML 구조 (<!DOCTYPE html>, <html>, <head>, <body>)를 만듭니다.

    • <head> 안에 CSS 스타일링을 위한 <style> 태그 또는 외부 CSS 링크를 추가합니다. (간단한 CSS 프레임워크 مثل Bootstrap, Tailwind CSS CDN 사용 가능하나, 복잡도 증가)

    • <head> 또는 <body> 끝에 <script> 태그를 추가하여 클라이언트 측 JavaScript 코드를 작성합니다.

    • <body> 안에 데이터 테이블(<table>), 필터링 옵션(dropdown <select>, input <input>), 정렬 버튼 등을 배치합니다.

<!-- 예시: Index.html (일부) -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <title>부동산 정보</title>
    <style>
      /* 기본적인 CSS 스타일링 */
      body { font-family: sans-serif; }
      table { border-collapse: collapse; width: 100%; }
      th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
      th { background-color: #f2f2f2; cursor: pointer; } /* 정렬 위해 커서 변경 */
      .filter-controls { margin-bottom: 15px; }
      /* ... 더 많은 스타일 ... */
    </style>
  </head>
  <body>
    <h1>아파트 시세 정보</h1>

    <div class="filter-controls">
      <!-- 예시 필터: 나중에 JS로 동적 생성하거나 값 설정 필요 -->
      <label for="region-filter">지역:</label>
      <select id="region-filter">
        <option value="">전체</option>
        <option value="강남구">강남구</option>
        </select>
      <label for="min-price-filter">최소 매매가:</label>
      <input type="number" id="min-price-filter" placeholder="예: 50000">
      <button onclick="applyFilters()">조회</button> 
    </div>

    <table id="dataTable">
      <thead>
        <!-- 테이블 헤더 (JS로 동적 생성 또는 직접 작성) -->
        <tr>
          <th onclick="sortTable(0)">단지명</th> 
          <th onclick="sortTable(1)">거래금액</th>
          <th onclick="sortTable(2)">전용면적</th>
          <!-- ... 나머지 헤더 ... -->
        </tr>
      </thead>
      <tbody id="dataTableBody">
        <!-- 데이터 행이 여기에 동적으로 삽입됩니다. -->
        <tr><td colspan="7">데이터 로딩 중...</td></tr> 
      </tbody>
    </table>

    <script>
      // 페이지 로드 시 데이터 가져오기
      document.addEventListener('DOMContentLoaded', function() {
        google.script.run.withSuccessHandler(displayData)
                          .withFailureHandler(onFailure)
                          .getDataForWebApp(); // 서버(GS) 함수 호출
      });

      // 서버로부터 받은 데이터로 테이블 채우기
      function displayData(data) {
        const tableBody = document.getElementById('dataTableBody');
        tableBody.innerHTML = ''; // 기존 내용 비우기

        if (!data || data.length === 0) {
          tableBody.innerHTML = '<tr><td colspan="7">데이터가 없습니다.</td></tr>'; // 헤더 수에 맞게 colspan 조정
          return;
        }

        // data는 2차원 배열 [[헤더1, 헤더2,...], [값1, 값2,...], ...] 형태 가정
        const headers = data[0]; // 헤더 정보 (필요시 사용)
        const rows = data.slice(1); // 실제 데이터 행들
        
        // 테이블 헤더 업데이트 (선택 사항)
        // const tableHead = document.querySelector('#dataTable thead tr');
        // tableHead.innerHTML = headers.map((header, index) => `<th onclick="sortTable(${index})">${header}</th>`).join('');

        rows.forEach(rowData => {
          const tr = document.createElement('tr');
          rowData.forEach(cellData => {
            const td = document.createElement('td');
            td.textContent = cellData; // 필요시 숫자 포맷팅 등
            tr.appendChild(td);
          });
          tableBody.appendChild(tr);
        });
      }
      
      // 필터 적용 함수 (예시)
      function applyFilters() {
        const region = document.getElementById('region-filter').value;
        const minPrice = document.getElementById('min-price-filter').value;
        
        const filters = {
          region: region,
          minPrice: minPrice
          // 기타 필터 값
        };
        
        // 로딩 표시 등 추가 가능
        document.getElementById('dataTableBody').innerHTML = '<tr><td colspan="7">데이터 로딩 중...</td></tr>'; 
        google.script.run.withSuccessHandler(displayData)
                          .withFailureHandler(onFailure)
                          .getDataForWebApp(filters); // 필터 값 전달
      }

      // 정렬 함수 (기본 클라이언트 측 정렬 예시 - 복잡하면 서버 측 정렬 고려)
      let sortColumn = -1;
      let sortAsc = true;
      function sortTable(columnIndex) {
         // 현재 테이블 데이터 가져오기 (DOM 기반) 또는 서버에 재요청
         // ... 정렬 로직 구현 ... (데이터 양 많으면 서버측 정렬 추천)
         // displayData() 다시 호출하여 정렬된 결과 표시
         alert("정렬 기능 구현 필요 (컬럼 인덱스: " + columnIndex + ")"); 
      }

      // 오류 처리 함수
      function onFailure(error) {
        const tableBody = document.getElementById('dataTableBody');
        tableBody.innerHTML = `<tr><td colspan="7">데이터 로드 실패: ${error.message}</td></tr>`;
        console.error("서버 함수 호출 실패:", error);
      }
    </script>
  </body>
</html>

4단계: 웹앱 제공 및 데이터 전달 (Apps Script - .gs 파일)

  1. doGet(e) 함수 작성: 웹앱 접근 시 호출되는 기본 함수입니다. HTML Service를 사용하여 Index.html을 반환합니다.

  2. 데이터 제공 함수 작성 (getDataForWebApp):

    • HTML 페이지의 JavaScript(google.script.run)에서 호출될 함수입니다.

    • Google Sheets에서 데이터를 읽어옵니다 (sheet.getDataRange().getValues()).

    • 필터링/정렬: 클라이언트에서 전달받은 파라미터(예: filters, sortInfo)를 기반으로 시트에서 읽어온 2차원 배열 데이터를 서버(Apps Script) 측에서 필터링하고 정렬합니다. 데이터 양이 많다면 이 방식이 클라이언트 측 부하를 줄여줍니다.

    • 가공된 데이터를 2차원 배열 형태로 반환합니다.

// 예시: Code.gs (웹앱 관련 부분)

function doGet(e) {
  // HTML 템플릿을 사용하여 HTML 페이지 생성 및 반환
  return HtmlService.createTemplateFromFile('Index') 
      .evaluate()
      .setTitle("부동산 정보 웹앱") // 브라우저 탭 제목 설정
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); // 필요시 iframe 임베딩 허용
}

// 웹 페이지(클라이언트 JS)에서 호출할 함수
function getDataForWebApp(filters) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(SHEET_NAME);
    let data = sheet.getDataRange().getValues(); // 시트 전체 데이터 가져오기 (헤더 포함)

    // --- 서버 측 필터링 로직 ---
    if (filters) {
      const headerRow = data[0]; // 헤더 행
      const dataRows = data.slice(1); // 데이터 행만 분리

      const filteredData = dataRows.filter(row => {
        let match = true;
        // 예시: 지역 필터 (단지명 또는 동 컬럼 기준 - 인덱스 조정 필요)
        if (filters.region && filters.region !== "") {
           const regionIndex = headerRow.indexOf('동'); // '동' 컬럼 인덱스 찾기 (없으면 -1)
           if (regionIndex !== -1 && row[regionIndex] !== filters.region) {
             match = false;
           }
        }
        // 예시: 최소 매매가 필터 (거래금액 컬럼 기준 - 인덱스 및 타입 변환 주의)
        if (filters.minPrice && filters.minPrice !== "") {
          const priceIndex = headerRow.indexOf('거래금액'); // '거래금액' 컬럼 인덱스
          const price = parseInt(row[priceIndex].replace(/,/g, '')); // 콤마 제거 후 숫자로 변환
          if (priceIndex !== -1 && (isNaN(price) || price < parseInt(filters.minPrice))) {
            match = false;
          }
        }
        // ... 다른 필터 조건 추가 ...
        return match;
      });
      
      data = [headerRow, ...filteredData]; // 헤더와 필터링된 데이터 합치기
    }
    
    // --- 서버 측 정렬 로직 (필요시 구현) ---
    // if (sortInfo) { ... data.slice(1).sort(...) ... }

    return data; // 필터링/정렬된 데이터 반환

  } catch (e) {
    Logger.log("데이터 가져오기 오류 (웹앱용): " + e);
    // 오류 발생 시 빈 배열 또는 오류 메시지 포함된 배열 반환 고려
    return [['오류 발생', e.message]]; 
  }
}

5단계: 웹앱 배포

  1. 스크립트 편집기에서 배포 > 새 배포를 클릭합니다.

  2. 유형 선택에서 웹 앱을 선택합니다.

  3. 설명을 입력합니다 (선택 사항).

  4. 웹 앱 설정:

    • 실행 사용자: 나 (API 호출 등 권한 필요 시)

    • 액세스 권한이 있는 사용자: 모든 사용자 (링크가 있는 누구나 접근 가능) 또는 [내 도메인 사용자]

  5. 배포 버튼을 클릭합니다.

  6. 필요한 권한을 검토하고 액세스 허용을 클릭합니다. (처음 배포 시 또는 권한 변경 시)

  7. 배포가 완료되면 웹 앱 URL이 표시됩니다. 이 URL로 접속하면 웹앱을 사용할 수 있습니다.

추가 고려사항 및 개선점:

  • 성능: 데이터 양이 매우 많아지면 Google Sheets에서 데이터를 읽고 Apps Script에서 처리하는 데 시간이 오래 걸릴 수 있습니다 (Apps Script 실행 시간 제한 있음). 이 경우 더 전문적인 백엔드/DB 사용을 고려해야 합니다.

  • UI/UX: Apps Script HTML Service는 기본적인 UI만 제공합니다. 스크린샷처럼 세련된 UI를 만들려면 CSS와 JavaScript 작업이 많이 필요하며, 반응형 디자인 등을 직접 구현해야 합니다.

  • 정렬/필터링: 복잡한 정렬/필터링 로직은 Apps Script에서 구현하기 까다로울 수 있습니다. 클라이언트 측 JavaScript 라이브러리(예: DataTables.net - 모든 데이터를 로드한 후 처리)를 사용하거나, 서버 측 로직을 신중하게 구현해야 합니다.

  • 오류 처리: API 호출 실패, 데이터 파싱 오류, 시트 접근 오류 등 다양한 예외 상황에 대한 처리를 꼼꼼하게 추가해야 합니다.

  • API 쿼터: 공공데이터 API 및 Apps Script 자체의 일일 할당량(쿼터)을 확인하고 초과하지 않도록 주의해야 합니다. (데이터 캐싱이 중요한 이유)

결과와 배운 점

데이터 분석할 때 파이썬 코딩을 바이브 코딩으로 하는건 했었지만, 웹앱을 만들어볼 생각은 해본 적이 없었어서 웹앱을 만들어본 것도 이번이 처음이고 Apps Script도 처음인데, 정확한 원리와 문법은 몰라도 일단 AI와 티키타카로 이것도 되는구나.. 의 경험과 느낌을 가져갑니다. ㅎㅎ

ChatGPT Plus 구독 중인데 Claude 좋다는 말들이 많아서 구독할까 하다가 (무료로는 금방 끊겨서 못쓰겠네요ㅠ), 무료인 Google AI Studio로 진행했는데 만족스럽네요. ㅎㅎ 구글 사랑합니다🧡

소규모로 진행된 스터디여서 각자 진행하는 내용 공유가 비교적 빨리 끝나는 편이라 남은 시간에는 Apps Script 실습시간을 가졌었는데요. 처음 해보는 저로서는 실습시간으로 가볍게 감을 잡게 되었던 것 같습니다. 특히 지난주에 스터디장이신 스마트 대디님이 즉석에서 시연하는 과정에서 오류와 해결과정을 직접 눈으로 보고 나니, "아, 이것도 결국 티키타카로 해결 가능하구나" 이런 느낌이 팍 왔네요. ㅋㅋ

다만 Apps Script는 처음 하다보니 살짝 막막함과 개인적인 사정이 살짝 있어서 초반에 시간 할애를 많이 못했더니 이번달은 제대로 뭔가 만들어보기도 전에 한달이 너무 빨리 가버렸네요.ㅠ

아무튼 '감'은 잡았으니(?.. 분명 오류나서 질문하고, 오류나서 질문하고 무한루프 돌겠지만;;) 일단 게시글 올려놓고, VOD 돌려보고 티키타카 무한루프 돌면서 만들어보고 게시글은 업데이트 해야겠습니다.

4
1개의 답글

👉 이 게시글도 읽어보세요