글 작성자: 택시 운전사
반응형

고장나버린 사이트

거의 방치하고 있던 더 많이 더 적게 프로젝트를 오랫만에 한 번 들어갔는데, 뭔가 문제가 생겼었다.

?!

"문제가 생겼습니다."라는 저 문구 저건 분명히 GraphQL 요청이 실패했을 때 뜨는 화면이였다. 타임어택 기능 이후로 방치는 하고 있었지만, 처음으로 흥한 소중한 사이드 프로젝트라 문제의 원인 파악에 나섰다.

문제는 내가 사용하고 있었던 GraphCMS라는 서비스에 있었다. GraphCMS의 과거 Lagacy 버전 프로젝트를 그대로 사용하고 있었는데, GraphCMS 측에서 Lagacy 프로젝트를 다 지워버린 것이다. 덕분에 사이트에 오류가 뜬 건 물론이고 내가 어렵게 모은 키워드 + 사진 + 검색량 자료도 날라갔다. 억울하긴 하지만, Lagacy 프로젝트를 계속 사용해 온 내 잘못도 있고 GraphCMS 측에서 경고도 계속 했을 것이다. 그렇게 더 많이 더 적게 사이트는 오류가 난 채로 굴러가고 있었다.(내 서버비와 도메인 비용도 날라갔고...) 얼마나 방치되었는지 구글에 "더 많이 더 적게"를 검색하면 "더 많이 더 적게 오류"가 연관 검색어인 수준이다.

그 동안 허탕치신 분들 죄송합니다...

살려야한다

살려야한다

이렇게 사이트를 방치할 수 없다고 생각해서 이 문제를 해결하기로 결심했다. 프론트 엔드 쪽 로직은 기존에 문제없이 잘 짜여져있으니 GraphQL로 가져오던 데이터를 어떻게 가져올 지가 문제였다. 기존에 사용했던 것처럼 CMS를 사용하는 방식은 이제 선택지에 없다. 이번 Lagacy를 없애버리는 케이스도 있고, 이전에 CMS를 조사하면서 느낀거지만 내가 원하는 목적 대비 고스펙에 취미 수준으로 사용하기에는 가격도 만만치 않았다. 그러다가 아주 간단하게 해결할 방법을 찾았다. 굳이 데이터를 외부에서 가져올 필요가 없었던 것이다. 프론트엔드 프로젝트에 static하게 데이터를 가지고 있다가 쓰는 것이다.

 

사실 프론트엔드 프로젝트에 static하게 데이터를 가지고 있는 방법을 프로젝트 초창기에도 사용했었다. 하지만 정말 멍청하게 이미지파일까지 프로젝트에 넣으면서 무수한 트래픽의 요청으로 허겁지겁 CMS를 이용해서 트래픽을 줄였었다. 그렇게 관성적으로 CMS를 사용하다가 이번 문제까지 생긴 것이다. 그래서 이번에는 프론트엔드 내의 static 데이터를 사용하면서 사진은 프론트엔드 프로젝트에 넣지 않는 방식으로 구현해야했다. 그래서 이미지 서버를 사용하기로 결정을... 하려 했으나 과거 프로젝트의 데이터를 만들면서 들었던 노가다가 생각났다. 과거의 노가다는 이러했다. 키워드를 정하고, 구글에 검색, 검색량 복사, 이미지 찾아서 이미지 다운 & CMS 업로드 이렇게 모든 키워드 반복이다. 이미지 서버를 사용하게 된다해도 노가다는 사라지지 않을 것이다. 오히려 다운 & CMS 업로드에서 이미지 URL 복사 붙여넣기 작업까지 추가되는 꼴이라 그 노가다를 다시는 하고 싶지 않았다. 그래서 찾은 것이 크롤링이다.

크롤링

키워드 별로 가지고 있는 데이터는 다음과 같았다.

  • name: 키워드명
  • count: 검색량
  • categories: 카테고리 목록(신규추가)
  • image.url: 이미지 주소

키워드 인터페이스

여기에서 키워드명과 카테고리 목록은 수작업으로 넣고 검색량(count)과 이미지 주소(image.url)는 크롤링을 통해 해결할 예정이다.

업데이트 전 키워드 예시

크롤링의 대상은 기존에 구글 데이터를 사용했기 때문에 구글을 대상으로 하였고, 검색량과 이미지 주소가 비어있는 전체 json를 넣으면 크롤링을 통해 해당하는 값을 업데이트해서 최종적으로는 해당 값들이 포함된 json을 얻게 되는 것이다. 크롤링 코드는 그나마 익숙한 파이썬으로 짰고, 인터넷에 올라온 여러 코드들을 짜깁기했다. 최종적으로 나온 코드는 다음과 같다. 시간이 여유롭지 않아서 코드 최적화나 논리에 이상한 부분이 생길 것 같아 코드는 안올리려했는데 누군가 도움이 되지 않을까 싶어서 올려본다.

from urllib.parse import quote_plus
from selenium import webdriver
import urllib.request
import re
import json
from time import sleep
import time
import os
from datetime import datetime


GOOGLE_SEARCH_BASE_URL = 'https://www.google.com/search?q='
GOOGLE_SEARCH_IMAGE_BASE_URL = 'https://www.google.co.kr/imghp?hl=ko'
TARGET_JSON_FILE = 'keywords.json'


def get_keywords():
    with open(TARGET_JSON_FILE) as json_file:
        json_data = json.load(json_file)
    return json_data


def initWebDriver():
    driver = webdriver.Chrome(executable_path='./chromedriver')
    return driver


def createFolder(directory):
    try:
        if not os.path.exists(directory):
            os.makedirs(directory)
    except OSError:
        print('Error: Creating directory. ' + directory)


def getKeywordCount(keywordName, driver):

    searchUrl = GOOGLE_SEARCH_BASE_URL + quote_plus(keywordName)

    keywordCount = 0

    while True:
        driver.get(searchUrl)

        # 검색어 횟수 획득
        try:
            keywordCount = int(driver.find_element_by_xpath(
                '//*[@id = "result-stats"]').text.split(' ')[2][0:-1].replace(',', ''))
            return keywordCount
        except:
            continue


def getImageUrls(keywordName, driver):

    driver.implicitly_wait(3)

    print(f'[{keywordName}] 검색')

    driver.get(GOOGLE_SEARCH_IMAGE_BASE_URL)

    Keyword = driver.find_element_by_xpath('//*[@id="sbtc"]/div/div[2]/input')
    Keyword.send_keys(keywordName)

    driver.find_element_by_xpath('//*[@id="sbtc"]/button').click()

    imageUrls = []

    i = 1
    try:
        while True:
            # 썸네일 이미지 클릭
            driver.find_element_by_xpath(
                '//*[@id="islrg"]/div[1]/div['+str(i)+']/a[1]/div[1]/img').click()

            # 원본 이미지 기다리기
            sleep(2)

            # 원본 이미지 url 획득
            imageUrl = driver.find_element_by_xpath(
                '//*[@id="Sva75c"]/div/div/div[3]/div[2]/c-wiz/div/div[1]/div[1]/div[2]/div[1]/a/img').get_attribute('src')

            # 이미지로딩이 완전히 되지 않는 경우가 있어서 이미지 url이 1000보다 낮아야 획득
            if len(imageUrl) < 1000:
                imageUrls.append(imageUrl)
                break
            driver.find_element_by_xpath(
                '//*[@id="Sva75c"]/div/div/div[2]/a').click()
            i += 1

    except:
        print(f'[{keywordName}] 링크 수집 실패')

    return imageUrls


def app():

    # chrome driver 시작
    driver = initWebDriver()

    keywords = get_keywords()

    print(f'키워드: 총 {str(len(keywords))}개')

    for index, keyword in enumerate(keywords):
        # 키워드
        keywordName = keyword['name']

        # 검색어 횟수 획득
        keywordCount = getKeywordCount(keywordName, driver)

        # 원본 이미지 링크들 획득
        imageUrls = getImageUrls(keywordName, driver)

        # 기존 키워드 배열 수정
        keywords[index]['image']['url'] = imageUrls[0]
        keywords[index]['count'] = keywordCount

        print(f'[{keywordName}] 업데이트 완료')

        sleep(1)

    with open(datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "_keywords.json", "w", encoding='utf8') as json_file:
        json.dump(keywords, json_file, ensure_ascii=False)
    print('키워드 전체 업데이트 완료')

    driver.close()


app()

업데이트 후 키워드 예시

추후 개선사항

결과적으로 업데이트도 잘 되고, 사이트도 정상화되었지만 몇가지 개선되었으면 하는 사항들이 있었다.

배포를 해야 키워드가 업데이트되는 사이트가 있다?

API를 안타다보니 키워드를 업데이트 할 때마다 항상 프로젝트를 배포해야한다. vercel를 이용하면 CI/CD가 자동으로 적용되어서, push하면 자동으로 배포되긴 하지만, 키워드 업데이트로 commit history가 오염된다. 해결 방법은 API 서버를 하나 두는 건데 서버 구축하기는 귀찮고... 구글 스프레드 시트 API를 사용을 고려해보고 있다. 다만 조회량의 제한이 있고 복잡한 오브젝트를 쓰기 힘들다는 점이 좀 아쉽다. 추가적으로 아무리 텍스트만으로 이루어진 스테틱데이터도 요청이 많아지면 트래픽이 꽤 쌓여서 금전적인 문제도 생긴다.

구글 이미지의 첫번째 이미지는 가장 좋은 이미지인가?

크롤링을 통해 구글 이미지 검색의 가장 첫번째 이미지를 가져오도록 했는데, 크기가 매우 작은 이미지나 가끔 구토를 유발하는 비율을 가진 이미지를 가져오는 경우가 있다.

블리츠 키워드로 가져온 이미지인데 비율이 쉣이다...

마무리

이번에 카테고리 필드를 추가했는데 카테고리별 더 많이 더 적게로 이용할 예정이다. 업데이트는 언제할지는 나도 모르겠다 ㅎㅎ 그때까지 많은 이용 부탁드린다

 

https://www.higherlowerkorea.com/

 

더 많이 더 적게

구글 검색량을 이용한 중독성 넘치는 검색량 비교 게임

higherlowerkorea.com

반응형