JS + Flask로 트위치 확장프로그램 만들기!!

junah·2022년 7월 29일
24
post-thumbnail

github : https://github.com/junah201/naver-cafe-twitch-extension

요약
1. 트위치 확장을 지인의 트위치 방송을 위해 만들기로 하였다.
2. 처음에는 리엑트로 만들려고 했는데 규모가 작고, Twitch CDN에 호스팅이 안되는 문제 때문에 바닐라JS로 돌아감.
3. 백엔드가 없는 것으로 설계를 하였으나 CORS 문제 때문에 Flask 서버를 만들게 됨.
4. 공식 디스코드에 질문하는 등 시행착오가 있었으나 결국 배포에 성공함.

경고
1. 글이 개발하면서 써서 그런지 매우 중구난방합니다.
2. 글을 아무 생각없이 써서 아무 생각없다는 것이 티가 납니다.
3. 수많은 뻘짓이 기록되어 있으므로 답답함을 유발할 수 있습니다.
4. 현직 개발자분께서 보시기에 매우 비효율적인 코드로 불편함을 유발할 수 있습니다.
5. 글에 맞춤법이 틀렸을 수도 있습니다.

프로젝트를 시작하게 된 계기

아는 지인이 트위치 방송을 하는데 항상 공지를 네이버 카페에다가 남겨서 해당 방송에 대한 공지를 보려면 카페에 직접 들어가야하는 불편함이 있어 위와 같이 트위치 확장 프로그램 패널로 만들어서 편하게 봐야겠다는 생각으로 시작하게 되었다.

트위치 확장은 어떻게 만드는거지..?

이 때는 진짜로 아무 생각이 없어서 어떤 언어로 만드는지도 모르지만 무작정 시작했다.
원래 모르는 것이 있을 때는 바로 구글에 How to make ~~

확장은 단순히 웹 페이지이며 유일한 기본 요구 사항은 확장 도우미를 가져오는 것입니다. 라는 문구를 통해 트위치 확장 패널이 사실상 다른 웹사이트를 띄우는 느낌이라는 것을 인지하게 되었고

추가로 트위치 확장 프로그램을 만들 수 있는 트위치 개발자 샌드박스를 찾아서 hello world를 실행해보았다.

이와 관련해서 트위치 확장 공식 문서를 찾아보니

  1. 트위치 개발자 콘솔에서 확장 프로그램 만들기를 해서 나의 확장을 만들기
  2. 트위치 개발자 콘솔에서 관련 정보를 등록하기
  3. 내가 직접 호스팅 한 후 그 도메인을 트위치에 알려주기

다음과 같은 절차로 진행되는 것 같았다.

이제 개발을 해봐야하는데...

JS HTML CSS 쪽 개발을 안한지 1년이 넘었고 애초에 개발을 본격적(?)으로 시작한지도 오래되지 않아서 웹 쪽에 대한 기초가 부족한 상태였다.

기초가 부족하면 강의로 매꿔야지

웹 쪽에 대한 4가지 강의를 찍먹만 하고 나머지는 야매로 구현했는데

  1. 알코 html css 강의
  2. 노마드 코더 JS 클론코딩
  3. Academind React 강의
  4. 코딩 애플 React 강의

진짜 기초만 대충 보고 나머지는 구글링으로 미래의 내가 해결하겠지 라는 마인드로 봤던 것 같다.

본격적인 개발

확장 프로그램 개발을 위해서 트위치에서 RIG 라고 따로 개발 프로그램(?)을 주는 것이 있어서 그 프로그램으로 직접 해볼려고 하니 헬로우월드 등 다양한 템플릿이 있어서 바로 헬로우 월드로 시작 but 음 구조가 욀케 복잡하지...

그냥 리엑트 프로젝트 새로 만들어서 적용해야겠다. ㅠㅠ
npm create-react-app my-app

네이버 카페 글을 가져와야하는데...

네이버 카페의 글을 가져오는 방법은 총 두가지가 있는데
첫번째는 직접 크롤링해서 가져오는 방법이고, 두번째는 네이버에서 제공하는 API를 사용하는 것이다.

이것만 본다면 당연히 네이버 API를 사용해야겠지만
네이버 API에는 큰 단점이 있었다. 바로 단순 검색어만 입력 가능하다는 것이다.
이말은 네이버 API에서 가져오는 글은 특정 카페의 특정 게시판에서 쓴 글 목록을 가져올 수 있는 것이 아니라
단순히 네이버 카페 검색창에 검색했을 때 나오는 결과가 나오기 때문에 구체적으로 내가 원하는 글을 가져올 수 없었다.

내가 원하는 것은 특정 카페, 특정 게시판 (ex. 공지 게시판)에 올라오는 글을 가져오는 것을 원하는 것이기 때문에
굳이 이 API를 사용하려면 모든 공지 글에 OOO의 공지 글 이런식의 키워드가 있어서 이 글을 딱 찾을 수있게 해야하는데
이 부분은 조금 불편한 느낌이 있기 때문에 일단 스킵하고 크롤링을 위한 방식을 찾으려고 노력했다.

이 때까지만 해도 단순히 네이버 API를 이용해서 가져오는 것인데 굳이 백엔드가 필요한가라는 생각이 머리 속을 지배하고 있었기 때문에 백엔드 서버를 둘 생각을 아예하지 않고 JS 내에서 크롤링 하기 위해서 라이브러리를 찾아보기 시작했다.

casperjs라는 라이브러리를 찾아서 어찌어찌 하다가 결국 실패해서 네이버 API로 다시 선회 하였다.
(이 때까지만 해도 굳이 백엔드를 만들어야겠다는 생각이 없었기 때문에 JS 언어 내에서 해결하고 싶었다.)

네이버 API 사용

axios를 npm install axios로 설치하려고 하니 다음과 같은 에러가 떠서 npm install axios --no-audit 으로 해결하였다.

axios를 이용해서 네이버 API에서 크롤링 하려고 하니 CORS라는게 나를 막아서서 이 부분에 대해서 구글링을 열심히 해보니 API와 JS 사이에 백엔드를 하나 두면 해결 된다는 것을 알고 Flask 서버를 이때부터 계획하기 시작했다.

UI 개발


내가 맨처음 구상했던 UI는 위 사진을 네이버 버전으로 바꾸기 위해서 초록색 테마를 주로 쓰고 거기에 다크 테마를 적용하는 것이 였지만 네이버 API로는 제목 밖에 크롤링이 불가능 했고 내부에 들어가서 값을 가져온다고 해도 나중에는 크롤링하는 방식으로 바꿀 예정 이였기 때문에 제목만 크롤링하여

다음과 같은 방식으로 간단하게만 구현하였다.

Flask 백엔드 개발

사실 백엔드에서 개발해야할 부분에 get 요청을 받으면 네이버 API로 API KEY 등을 포함한 get 요청을 보내고 받은 데이터를 가공해서 다시 보내주는 것 이외에는 딱히 개발할 내용이 없었다.

import flask
import requests
import os, json

config = {
    "X-Naver-Client-Id": os.environ["X-Naver-Client-Id"],
    "X-Naver-Client-Secret": os.environ["X-Naver-Client-Secret"]
}

app = flask.Flask(__name__)


@app.route('/', methods=["GET"])
def main():
    return "Hello World"


@app.route('/naver', methods=["GET"])
def naver():
    parameter = flask.request.args.to_dict()
    if len(parameter) == 0:
        return "query 파라미터는 필수적으로 입력되어야 합니다."

    response = requests.get(
        f"https://openapi.naver.com/v1/search/cafearticle.json?query={parameter['query']}&sort=date&display=15", headers={
            "X-Naver-Client-Id": config['X-Naver-Client-Id'],
            "X-Naver-Client-Secret": config['X-Naver-Client-Secret']
        })

    my_res = flask.Response()
    # CORS 이슈 해결
    my_res.headers.add("Access-Control-Allow-Origin", "*")
    # 검색어는 볼드 처리 되서 출력되기 때문에  <b> <\/b> 를 제거
    result = ((response.text).replace("<\/b>", "")).replace("<b>", "")
    my_res.set_data(result)

    return my_res


if __name__ == '__main__':
    app.run(threaded=True, port=5000)

React 에서 무한로딩 문제

React 에서 http get으로 가져온 데이터를 useState 를 이용해서 넣으니 get 요청을 무한으로 보내는 문제가 있었다.

/* 문제가 있던 코드 */

const App = () => {
	const [data, setData] = useState();
    
    fetch(url, {
        method: 'GET',
    })
    .then(response => {
        setData(response.json());
    })
    
    return
    (
    	<div>
        	for ( let i = 0; i < data.items.length; i++) 
            {
              <div>
              	<a href = {data.items.[i].link}>{data.items.[i].title}</a>
              </div>
            }
        </div>
    )
}

어라?
분명 처음 get 요청 한번만 들어가야하는데 뭐가 문제지? 하고 보니
setData로 값이 변경되게 되면 해당 렌더링 함수를 다시 호출하게 됨으로써 무한루프가 돌게 된다고 하더라고요
결국 구글링을 통해 useEffect를 이용하여 해결하였다.

/* 해결한 코드 */

const App = () => {
	const [data, setData] = useState();
    
    useEffect(() => {
      	fetch(url, {
        	method: 'GET',
   		})
        .then(response => {
        	setData(response.json());
        })
    }, []);
    
    return
    (
    	<div>
        	for ( let i = 0; i < data.items.length; i++) 
            {
              <div>
              	<a href = {data.items.[i].link}>{data.items.[i].title}</a>
              </div>
            }
        </div>
    )
}

나중에 결국 바닐라JS로 돌아가게 됨으로써 안쓰게 되게 버렸다...

이제 트위치에 업로드 해야되는데...

이 트위치에 업로드하는 과정에서 수많은 시행착오가 있었는데

내가 결국 이해한 업로드 과정은 다음과 같다.

1. npm build 등으로 dist 파일을 만들기
2. 이 파일을 전부 하나의 파일로 압축하기
3. 이 압축파일을 twitch CDN에 업로드 하여 호스팅하기

이 과정 중에서 압축파일을 업로드하고 호스팅을 확인하는 과정에서 나는 당연히 기본 url이라고 적힌 곳에서 호스팅을 테스트하는 것인줄 알고 해당 url로 들어가면

다음과 같은 NoSuchKey 에러가 떠서 내가 빌드를 잘못한 줄 알고 바닐라JS로 다시 제작하기 시작하였다.

바닐라JS

내가 리엑트 경험이 부족해서 일부 빼먹은 것이 있나 싶어서 테스트 해볼겸 바닐라JS로 넘어가게 되었다.
사실 다 만들고 나니 사실 리엑트 안해도 충분할 것 같은데 라는 생각이 들어서 넘어간 것도 있다.

const query = "검색어";
const url = `내서버URL/naver?query=${query}}`;

function reloadItem() {
  fetch(url, {
    method: "GET",
  })
    .then((response) => {
      return response.json();
    })
    .then((data) => {
      for (var idx of data.items.keys()) {
        const item = document.getElementById(idx);
        if (item == null) {
          const table = document.getElementById("table");
          if (idx == 0) {
            table.innerHTML +=
              `<div class = "item" id = ${idx}><a href = ${data.items[idx].link}>${data.items[idx].title}</a></div>`;
          } else {
            table.innerHTML +=
              `</br><div class = "item" id = ${idx}><a href = ${data.items[idx].link}>${data.items[idx].title}</a></div>`;
          }
        } else {
          item.innerHTML = `<a href = ${data.items[idx].link}>${data.items[idx].title}</a>`;
        }
        console.log(data.items[idx]);
      }
      var t = document.getElementById(0);
    });
}

document.addEventListener("DOMContentLoaded", reloadItem);
document.getElementById("refresh").addEventListener("click", reloadItem);

이렇게 다 만들고 다시 업로드 했는데 또 NoSuchKey 에러가 뜬다...
왜지..?

결국 TwitchDev 디스코드 서버에 찾아가서 직접 질문하기로 하였다.

TwitchDev 디스코드 서버 방문

직접 질문해보았더니 제공받은 기본URL에서 테스트하는 것이 아니라 직접 내 채널에 추가해서 테스트 하는 것이라고 해서 내 채널에 가서 확인해보니 살짝 깨지긴 했지만 정상적으로 출력되는 것을 볼 수 있었다.

어라 정상적으로 호스팅 했으나 get 요청이 안된다..?

위 이미지와 같은 에러가 떠서 다시 한번 디스코드 서버에 물어보니 도메인 허용 목록에 추가해야된다는 사실을 알고

추가하였더니 다시 정상적으로 출력이 된다.

스트리머들이 개인 패널을 설정할 수 있는 config.html 개발

디자인은 모르겠고 패널 제목과 검색할 키워드를 지정할 수 있도록 개발하였다...
저장 버튼을 누르면 저장되었습니다. 라는 문구가 뜨며 MySQL 서버로 값을 전송하도록 개발하였다.

const url = `https://어쩌구저쩌구.herokuapp.com`;

function saveConfig(channelId, title, keyword) {
  fetch(
    `${url}/config?channel_id=${channelId}&title=${title}&keyword=${keyword}`,
    {
      method: "POST",
    }
  ).then((response) => console.log(response));

  console.log("Saved");
}

window.Twitch.ext.onAuthorized(function (params) {
  const channelId = params.channelId;
  console.log(channelId);

  saveButton = document.getElementById("save-button");
  saveLabel = document.getElementById("save-label");

  fetch(`${url}/title?channel_id=${channelId}`, {
    method: "GET",
  })
    .then((response) => {
      console.log(response);
      console.log(response.text);
      return response.json();
    })
    .then((data) => {
      console.log(data);
      titleInput = document.getElementById("title-input");
      titleInput.value = data.title;
    });

  fetch(`${url}/keyword?channel_id=${channelId}`, {
    method: "GET",
  })
    .then((response) => {
      console.log(response);
      console.log(response.text);
      return response.json();
    })
    .then((data) => {
      console.log(data);
      keywordInput = document.getElementById("keyword-input");
      keywordInput.value = data.keyword;
    });

  saveButton.addEventListener("click", function () {
    saveLabel.innerHTML = "";
    const title = document.getElementById("title-input").value;
    const keyword = document.getElementById("keyword-input").value;
    if (title == "" || keyword == "") {
      saveLabel.innerHTML = "모든 값을 입력해주세요.";
      return;
    }
    saveConfig(channelId, title, keyword);
    saveLabel.innerHTML = "저장되었습니다.";
  });
});

이제 백엔드를 호스팅해야하는데...

처음에는 오라클에서 램을 6GB나 주는 평생 무료가 있다고 해서 오라클 우분투 클라우드로 만들어서 하려고 했으나 내부에서 방화벽을 모두 열고, 오라클 사이트에서까지 열었는데도 포트가 안열려서 오라클은 포기하고...

그냥 간단하게 호스팅 해주는 Heroku로 넘어갔다.

단순히 github에 커밋하면 자동으로 배포가 되는 등 저번에 사용해본 기능들이 좋아서 넘어간 것도 있었다.
음...헤로쿠에 배포할 경우 내부 DB로 sqlite3 등의 파일 기반 DB는 사용 불가능하다는 단점이 있었기에 ClearDB MySQL을 사용하였다. 저번 글에 보면 파이썬에서 SQL 사용법을 몰라서 모듈 사용법을 익히기 싫어서 SQL을 안썼다는 말이 있는데 이번에는 pymysql 모듈의 사용법을 익혀서 SQL을 이용하여 개발을 하니 json을 임시 DB로 사용할때는 몰랐던 WHERE 구문의 편의성을 체감할 수 있었다.

개발 후기

이렇게 트위치 확장 프로그램 개발이 무사히 완료되었다. 처음 시작할 때는 만드는 것이 어려울 것이라고 걱정했었지만, 공식 트위치 개발자 디스코드에서 사소한 질문까지 너무 친절하게 도와줘서 생각보다 쉽게 끝났다. 물론 질문 없이 구글링만으로 해결하려고 날린 뻘짓이 좀 크긴하지만 해외 디스코드 서버라서 영어로 질문하는 것에 대한 막연한 두려움이 있었는데 생각보다 어렵지 않고 고인물 개발자들은 전부 친절한 것 같았다.

날 도와준 Ambassador Barry Carlyon 개발자님 사랑해요 !!

profile
개발자를 꿈꾸는 사람

1개의 댓글

comment-user-thumbnail
2022년 8월 8일

트수퀄리티 상승

답글 달기