# 3.1-5. [Apps Script+네이버 API] 활용한 쇼핑 크롤링 실습

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/143155_oFA9cMCs35kxWv1t7P?q=80&s=1280x180&t=outside&f=webp)

```
네가 위에서 만들어준 아래 스크립트를 구글 colab이 아니라, Apps Script에서 구현이 가능해. 
Apps Script를 활용해서 index.html에서 키워드와 검색 개수를 입력받고, 스크립트 진행 후에, 결과를 구글 sheet로 받는 것을 의미하는 거야. 
---
- 네이버 쇼핑 검색 API로 원하는 개수(최대 1000개)만큼 수집하고,
중고·렌탈·해외직구/구매대행을 제외하며, 결과를 Pandas DataFrame → CSV(영문 파일명) 으로 저장하는 스크립트
```

> GPT

좋죠! **Apps Script 웹앱**으로 동작하도록 예시를 드릴게요.
`[index.html](https://index.html)`에서 키워드/수집 개수를 입력 → 서버(`[Code.gs](https://Code.gs)`)가 네이버 쇼핑 API를 **페이지네이션**으로 호출 → **중고/렌탈/해외직구·구매대행 제외** → 결과를 **구글 시트**에 기록합니다.

---

# 1) Code.gs

```
/** ===== 설정 =====
 * 1) 실행 전 '스크립트 속성'에 아래 키를 저장하세요.
 *    NAVER_CLIENT_ID, NAVER_CLIENT_SECRET
 *    (앱스스클: 상단 톱니바 > 프로젝트 설정 > 스크립트 속성)
 */
const PROP = PropertiesService.getScriptProperties();
const CLIENT_ID = PROP.getProperty('NAVER_CLIENT_ID');
const CLIENT_SECRET = PROP.getProperty('NAVER_CLIENT_SECRET');

const API_URL = 'https://openapi.naver.com/v1/search/shop.json';
const MAX_TOTAL = 1000;
const MAX_PER_REQUEST = 100;

/** 웹앱 진입 (index.html 렌더) */
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('Naver Shopping Crawler');
}

/** 공용 유틸: 태그/엔티티 정리 */
function cleanText(s) {
  if (!s) return '';
  // <b> 태그 및 기타 태그 제거
  s = s.replace(/<\/?b>/gi, '').replace(/<[^>]+>/g, '');
  // HTML 엔티티 디코딩
  return s.replace(/&amp;/g,"&")
          .replace(/&lt;/g,"<")
          .replace(/&gt;/g,">")
          .replace(/&quot;/g,'"')
          .replace(/&#39;/g,"'");
}

/** 공용 유틸: 영문/숫자/언더스코어 슬러그 */
function slugifyAscii(text) {
  return (text || 'query')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '_')
    .replace(/_+/g, '_')
    .replace(/^_+|_+$/g, '');
}

/** 핵심: 네이버 쇼핑 검색 + 시트 쓰기 */
function fetchShoppingToSheet(query, totalCount) {
  if (!CLIENT_ID || !CLIENT_SECRET) {
    throw new Error('스크립트 속성에 NAVER_CLIENT_ID / NAVER_CLIENT_SECRET를 설정하세요.');
  }
  if (!query) throw new Error('검색 키워드는 비어 있을 수 없습니다.');

  totalCount = Math.max(1, Math.min(Number(totalCount) || 0, MAX_TOTAL));

  const headers = {
    'X-Naver-Client-Id': CLIENT_ID,
    'X-Naver-Client-Secret': CLIENT_SECRET
  };

  const out = [];
  let fetched = 0;

  while (fetched < totalCount) {
    const remaining = totalCount - fetched;
    const display = Math.min(MAX_PER_REQUEST, remaining);
    const start = fetched + 1; // 1-base, 최대 1000

    const params = {
      query: query,
      display: display,
      start: start,
      sort: 'sim',
      // 요구사항: 중고/렌탈/해외직구·구매대행 제외
      exclude: 'used:rental:cbshop'
    };

    const url = API_URL + '?' + toQuery(params);
    const res = UrlFetchApp.fetch(url, { method: 'get', headers: headers, muteHttpExceptions: true });
    const code = res.getResponseCode();
    if (code !== 200) {
      throw new Error('네이버 API 오류: HTTP ' + code + ' / ' + res.getContentText());
    }

    const data = JSON.parse(res.getContentText());
    const items = (data && data.items) || [];
    if (!items.length) break;

    items.forEach(it => {
      out.push([
        cleanText(it.title),
        it.link || '',
        it.image || '',
        it.lprice || '',
        it.hprice || '',
        it.mallName || '',
        it.brand || '',
        it.maker || '',
        it.category1 || '',
        it.category2 || '',
        it.category3 || '',
        it.category4 || ''
      ]);
      fetched++;
    });

    // API 예의상 살짝 휴식
    Utilities.sleep(120);
  }

  // === 시트에 기록 ===
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheetName = 'naver_shopping_' + slugifyAscii(query) + '_' + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd_HHmmss');
  const sh = ss.insertSheet(sheetName);

  const header = [
    'title','link','image','lprice','hprice','mallName',
    'brand','maker','category1','category2','category3','category4'
  ];
  sh.getRange(1,1,1,header.length).setValues([header]);

  if (out.length) {
    sh.getRange(2,1,out.length, header.length).setValues(out);
  }

  // 자동 너비
  sh.autoResizeColumns(1, header.length);

  return {
    sheetUrl: ss.getUrl(),
    sheetName: sheetName,
    rowCount: out.length
  };
}

/** 쿼리스트링 유틸 */
function toQuery(obj) {
  return Object.keys(obj)
    .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(obj[k])))
    .join('&');
}
```

---

# 2) index.html

```
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Naver Shopping Crawler</title>
    <style>
      body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 2rem; }
      label { display:block; margin: .5rem 0 .25rem; }
      input[type="text"], input[type="number"] { width: 320px; padding: .5rem; }
      button { padding: .6rem 1rem; margin-top: 1rem; }
      .log { margin-top: 1rem; white-space: pre-wrap; }
    </style>
  </head>
  <body>
    <h2>네이버 쇼핑 수집 (중고·렌탈·해외직구/구매대행 제외)</h2>

    <label>검색 키워드</label>
    <input id="q" type="text" placeholder="예: 딸기잼">

    <label>수집 개수 (최대 1000)</label>
    <input id="n" type="number" min="1" max="1000" value="200">

    <div><button onclick="run()">수집 시작</button></div>

    <div class="log" id="log"></div>

    <script>
      function log(msg) {
        document.getElementById('log').textContent = msg;
      }
      function run() {
        const q = document.getElementById('q').value.trim();
        const n = Number(document.getElementById('n').value || 0);
        if (!q) { log('검색 키워드를 입력하세요.'); return; }
        if (!n || n < 1 || n > 1000) { log('수집 개수는 1~1000 사이 정수여야 합니다.'); return; }

        log('수집 중... 잠시만 기다려주세요.');
        google.script.run
          .withSuccessHandler(res => {
            log(`완료! ${res.rowCount}건 기록됨\n시트: ${res.sheetName}\n\n스프레드시트 열기:\n${res.sheetUrl}`);
          })
          .withFailureHandler(err => {
            log('오류: ' + (err && err.message ? err.message : err));
          })
          .fetchShoppingToSheet(q, n);
      }
    </script>
  </body>
</html>
```

---

## 배포/사용 방법 (요약)

1. **Apps Script 새 프로젝트** → 위의 `Code.gs`, `index.html` 추가.

2. **스크립트 속성**에

- `NAVER_CLIENT_ID`

- `NAVER_CLIENT_SECRET` 입력.

3. **웹앱 배포**: 배포 ▶ 웹앱 → "앱 실행: 본인", "접근권한: 본인(또는 조직)" → 배포 URL 열기.

4. 웹앱에서 키워드/수량 입력 → 실행 → **현재 스프레드시트**에 `naver_shopping_<slug>_<timestamp>` 시트로 결과가 저장됩니다.

> 참고

- API 파라미터: `display ≤ 100`, `start ≤ 1000`. 위 코드는 이를 고려해 루프 호출합니다.

- 제외 옵션: `exclude=used:rental:cbshop` (중고/렌탈/해외직구·구매대행 제외).

- 정렬은 `sim`(정확도). 최신순은 `date`로 교체하세요(`Code.gs`의 `sort` 값).

- 구글 Sheet > Apps Script 오픈

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/143657_AJ87T5wwbMKuqqS5Xo?q=80&s=1280x180&t=outside&f=webp)

- Code.gs와 index.html 생성 및 GPT 코드 ctrl c, ctrl v
- 

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/143917_6Op0OVoaVFL4Yxe0zD?q=80&s=1280x180&t=outside&f=webp)

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/143842_sV1DSmcapNgarYHgnY?q=80&s=1280x180&t=outside&f=webp)

- 스크립트 속성에 NAVER_CLIENT_ID, NAVER_CLIENT_SECRET 삽입하는 방법

    - 이렇게 하면 소스 코드에 ID저장되지 않아서 보안상 더 안전함

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/142225_kt8HyFWMYcJ1DvC1D8?q=80&s=1280x180&t=outside&f=webp)

- Apps Script 저장 > 실행

    - 버그 발생시에는 오류(error) 내용을 카피해서 GPT에게 그대로 문의

- Apps Script 배포

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/144143_fOEugjO44zgflfgYFA?q=80&s=1280x180&t=outside&f=webp)

- 웹 앱 + 액세스 권한 설정

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/144223_DMhhVEx1NlKrlqp4Wz?q=80&s=1280x180&t=outside&f=webp)

- 검색 화면 : 웹 앱 URL 

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/144432_HEZxw3bgl1YH224feG?q=80&s=1280x180&t=outside&f=webp)

- 위 URL 접속 > '블루투스 이어폰' 검색 예시

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/144518_XxZ7QPasXj6coL22fK?q=80&s=1280x180&t=outside&f=webp)

- 스프레드시트 열기 (맨 처음 시작했던 스프레드시트와 동일함)

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/144855_2hybIZ5h5mvUzdFAeY?q=80&s=1280x180&t=outside&f=webp)

- 추가 데이터 분석 진행

---

- 기타 응용

    - index.html > 더 예쁘고 깔끔하고, 고도화(조건화) 가능

        - 중고/렌탈/해외직구·구매대행

        - 정확도, 최신순

    - 네이버 API : 로그인 API, 서비스 API 등 다양한 API를 오픈해 두고 있음

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/150850_qFthjNxjHlJzDTAKGF?q=80&s=1280x180&t=outside&f=webp)

- 다른 검색 API 활용 가능

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/150100_cTuXHaFAzJvLwtZrnc?q=80&s=1280x180&t=outside&f=webp)

- 검색 API 외에 데이터랩 API

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/150700_1qJok1lfEqDniWKnoj?q=80&s=1280x180&t=outside&f=webp)

        - 데이터랩 API 활용 

![Image](https://upload.cafenono.com/image/slashpageHome/20251031/145923_wYyYVjnxNfPnuZZiOq?q=80&s=1280x180&t=outside&f=webp)

For the site tree, see the [root Markdown](https://slashpage.com/lion.md).
