Mocha와 Should.js를 이용해 테스트 코드를 작성하던 중, 콜백 함수 내에선 assert 기능이 정상적으로 작동하지 않는 것을 발견했다. 처음엔 Should.js 자체의 문제인 줄 알고 assert 라이브러리로 바꿔서 시도했지만 똑같은 현상이 발생했다. 아래와 같이 명백히 오류가 나야 하는 부분도 문제 없이 통과가 되었다.

 

const tempAndHum = require('tempAndHum')

it('현재 온습도 출력 테스트', () => {
    tempAndHum.getNowTah((data) => {
        (0).should.be.equal(1)
    })
})

 

 찾아본 결과, mocha에서 비동기 함수 내에서 테스트를 진행할 때는 done을 이용하여, mocha가 해당 테스트 함수가 모두 실행될 때까지 기다리도록 해야 한다. 그래서 다음과 같이 바꿨다. 여기까지는 공식 문서에도 설명이 잘 되어 있는 부분이다.

it('현재 온습도 출력 테스트', (done) => {
    tempAndHum.getNowTah((data) => {
        (0).should.be.equal(1)
        done()
    })
})

 

 

 이렇게 하고 테스트를 진행하니, 이제는 다음과 같이 엉뚱한 부분에서 에러가 떴다.

Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.

   

  분명 나는 done()을 넣어 놨는데, 테스트 모듈에선 못 찾고 있었다. 그래서 다시 찾아본 결과, (0).should.be.equal(1)의 assert 문에서 이미 Error가 던져져서 코드 실행이 중단되고, 따라서 done()까지 코드가 가지 못하고 있는 상태였다. Promise를 이용한 비동기 함수라면 .catch로 해결하면 될 것이고, 콜백 함수를 이용한 비동기 문일 경우 다음과 같이 try ~ catch 문으로 테스트 함수를 감싸면 된다. 최종적으로 아래와 같다.

 

it('현재 온습도 출력 테스트', (done) => {
    tempAndHum.getNowTah((data) => { // 테스트를 원하는 함수
        try {
            // 원하는 테스트 코드 입력
            done()
        }
        catch (err){
            done(err)
        }
    })
})

 

 

 

 Express.js를 이용하여 개발을 하면서 ORM 라이브러리로 Sequelize를 사용하고 있는데, 내 예상과 다르게 작동하는 부분이 많아서 조금 애를 먹고 있었다. 그 중에서 특히 내가 가장 삽질했던 부분을 공유하고자 한다.

 

 

Sequelize 모델 불러오기

 우선 가장 먼저 ORM을 이용하려면 내 코드에 해당 모델을 불러와야 하는데, 대부분의 문서나 글에선 이미 모델을 불러온 걸 전제로 CRUD를 설명하는데, 이 모델을 가져오는 부분에서부터 막혔었다.

 

 

 

 Sequelize-cli를 이용하여 다음 명령어를 이용하여 기본적인 파일을 생성했다.

 

sequelize init

 

 이렇게 하면, models/index.js에서 실제 DB와 시퀄라이저가 연결된다. 그런데, index.js에는 모델을 불러오는 형식이 이미 지정되어 있고, 따라서 각 모델 파일들의 형식도 다음과 같이 지정되어 있다. 물론, 이러한 형태에서는 require(user.js) 형태로 불러와도 당연히 해당 모델을 쓸 수 없다. 그렇다고 다시 싹 다 Sequelize 모듈을 임포트 하기엔 지저분하고 비효율적이다. 분명 이렇게 쓰라고 만든 기능이 아닐 것이다...

// models/user.js

module.exports = function (sequelize, DataTypes) {
    return sequelize.define(
        // ...
    )
}

 

 

 

 

 여기서, models/index.js 파일을 다시 보면 재미있는 사실을 찾을수 있다.

 

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};

// DBMS와 연결
let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}


// 각 모델을 불러옴.
fs
  .readdirSync(__dirname)
  .filter(file => {
    return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
  })
  .forEach(file => {
    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

 "각 모델을 불러옴"이라고 주석을 넣은 부분의 코드를 읽어 보면, 현재 index.js가 있는 폴더 내의 파일을 모두 읽고(자기 자신 제외), 이를 전부 연동(associate)하여 db라는 객체 안에 넣는다. 그리고 이 db를 익스포트한다. 그렇다면, 당연히 익스포트된 db 객체만 가져와도 DB의 모든 기능을 이용할 수 있을 것이다.

 

 

 

 따라서, 다른 파일에서 모델을 사용한다면...

const db = require('../models')
const tah = db["TempAndHum"] // 내가 만든 DB 이름

 

 이런 식으로 DB 이름을 이용하여 불러올 수 있다. models/index.js 안에서 DBMS 연결과 모델 연결을 모두 해 주기 때문에, 다른 데에선 그냥 DB 이름만 갖고 불러오면 된다. DB 이름은, 

module.exports = function (sequelize, DataTypes) {
    return sequelize.define(
        "TempAndHum", {
        id: { ...
        

 이렇게 각 모델 파일에서 정의한 이름을 따른다.

 

 

서브쿼리를 이용하여 조건을 걸고 DELETE 사용하기

 

  나는 특정 테이블을 생성 시간순으로 정렬하여, 거기서 특정 몇 개의 항만 삭제하는 함수를 구현하고자 했다. 일반적인 SQL 문에서는 서브 쿼리를 이용하여 구현할 수 있는데, 시퀄라이저에서는 서브쿼리를 다루는 방법이 좀 복잡하고 직관적이지 않았다. 그래서 그냥 직접 SQL 쿼리를 박아넣었다.

 

const db = require('../models')
const tah = db["TempAndHum"]

tah.destroy({
        where: {
            id: [Sequelize.literal(`SELECT * FROM (SELECT id FROM tempandhum ORDER BY createdAt DESC LIMIT 3) AS tmp`)]
        }
    })

 

 Sequelize.literal 함수를 통해 썡 SQL 쿼리를 넣었고, 내가 원하는 조건의 id들을 추출한 뒤 where 조건문에 넣어 조건에 따른 삭제를 수행한다. 쿼리를 SELECT * FROM ~~ AS tmp와 같이 래핑했는데, mariaDB에서는 서브쿼리에 limit가 적용이 안되서 위와 같이 편법으로 우회했다. 위 쿼리가 실제도 수행될 땐 다음과 같이 수행된다.

 

DELETE FROM `TempAndHum` WHERE `id` IN 
(SELECT * FROM (SELECT id FROM tempandhum ORDER BY createdAt DESC LIMIT 3) AS tmp)

 

'프로그래밍 > Node.js' 카테고리의 다른 글

Mocha에서 콜백 함수 테스트하기  (0) 2021.01.28

  Django에서 아임포트를 이용한 결제 시스템을 만들던 중, 문제가 있었다. 결제를 완료한 후, 결제 정보를 POST method를 통해 서버로 보내야 하는데, 서버로 보내도 항상 받은 데이터가 empty로 표시가 되었다. 처음에는 프론트엔드 쪽에서 문제가 있는줄 알았는데, 알고 보니 Django의 view에서 POST를 받을 때 모든 데이터를 받을 수 있는 게 아니었다.

 

  아래는 장고 공식 문서에서 Request.POST의 설명 중 일부이다.

https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.POST

 

A dictionary-like object containing all given HTTP POST parameters, providing that the request contains form data. See the QueryDict documentation below. If you need to access raw or non-form data posted in the request, access this through the HttpRequest.body attribute instead.

 

Request and response objects | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

  쉽게 말해, POST를 통해 form 데이터 이외의 데이터들은 request.POST.('요소') 와 같은 방식으로 받을 수 없고 request.body와 같이 바이트 스트림으로 직접 받아와서 처리해야 한다. 다행히, 생각보다 어렵진 않았다.

 


 

  나는 딱히 이미지와 같은 바이너리 형식의 데이터를 전송하는 게 아니므로, json 형태로 데이터를 주고받기로 약속했다. 보내는 쪽에서 json 형태로 보내도록 아래와 같이 코딩하자.

 

var paymentdata = {
    // 서버로 보낼 데이터(dictionary 형태)
}

jQuery.ajax({
      url: "{% url 'destination' %}",
      type: "POST",
      dataType: "json",
      data: JSON.stringify(paymentdata)
}).done(function (data) {
      alert("결제가 성공적으로 진행되었습니다.");
}).fail(function (data) {
      alert("결제중 서버와의 통신에 문제가 발생하였습니다.\n원인: " + data.reason);
})

 

 보내는 코드는 데이터만 json으로 확실하게 보낸다면 어떤 형태이든 상관없다.

받는 view 부분 역시 이 두줄이면 충분하다.

    import json
    
    # 생략
    
    if request.method == 'POST':
        payment_data = json.loads(request.body)

 

json으로 보내고 json으로 받는 점만 기억하면 큰 문제가 없을 것이다.

  위와 같은 스트링 데이터 이외의 이미지 등의 바이너리 데이터들을 주고받을 때에도 똑같은 방식으로 request.body를 이용해 처리하면 큰 문제 없을 것이다.

 


 

  추가로, 장고 기본 form을 보면 항상 {% csrf_token %} 이 있었던 걸 기억할 것이다.

아마 위 상태 그대로 form을 보낸다면 csrf 때문에 에러가 날 것이다.

이건 말 그대로 프론트엔드에서 보낼때 해당 토큰을 추가하면 되고,

 

jinmay.github.io/2019/04/09/django/django-ajax-csrf/

 

Django에서 ajax post 요청시 csrf token 문제 해결하기

Django에서 ajax 사용할때 발생하는 csrf 문제장고에서 ajax를 조금씩 섞어서 사용하다보면 POST 요청을 보낼때 문제가 발생하게 된다. POST 요청에서는 보안상의 문제로 csrf token을 필요로하게 되는게 �

jinmay.github.io

  이 블로그에 코드를 비롯하여 해결책이 명확하게 나와 있으니 참고하면 되겠다.

참고로 1번은 저 블로그 주인장분도 설명하셨지만 보안상으로 매우 안좋은 방법이니까 2번 방법을 강력히 권장한다.

문 열기 귀찮다고 현관문을 없애버리는 사람은 없을 것이다.

 

 

 

 

 

 django에는 기본적으로 email 발송 기능이 내장되어 있다. 그런데, 인터넷에 관련 예제를 찾아 보면 전부 gmail 발송이고, daum 메일(hanmail.net / daum.net) 발송에 대한 예제는 거의 없어서 정리하게 되었다.

 

 이미 gmail로 발송을 해 봤다면 기본적인 맥락은 거의 유사하다.

 

 

 

1. Daum 메일 설정

 

daum 메일 -> 환경설정에 들어가자.

 

 

 웹서버를 통한 메일 발송은 SMTP라는 프로토콜을 사용하게 되는데, 그러려면 다음과 같이 사용 설정을 해야 한다. 기본값으로 '사용 안 함'으로 되어있으니, 꼭 잊지 말고 바꿔야 한다.

 

 

 

 

2. django의 settings.py 변경

 

EMAIL_HOST = 'smtp.daum.net' 		 # 메일 호스트 서버
EMAIL_PORT = 465 			 # 서버 포트
EMAIL_HOST_USER =  '사용할 이메일'
EMAIL_HOST_PASSWORD = '이메일 비밀번호'
EMAIL_USE_SSL = True
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER	 # 기본 발신자
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

 

 

SMTP 설정 페이지 바로 밑에 나온 안내문이다. 이대로 설정하되, 이미 gmail로 구현해 본 적이 있다면 다른 점이 몇개 있다.

 

  • Port : gmail은 587 포트를 사용하는데 여기서는 465 포트를 사용해야 한다.
  • EMAIL_USE_SSL : 내가 이 글을 쓰게 된 이유이다. 처음에 gmail을 쓰다가 다음 메일로 교체했는데, 어떤 오류도 없이 그냥 이메일 발송 페이지에서 웹서버가 멈춰 버리는 일이 있었다. 값을 쭉 점검해본 결과, 이 설정값에 문제가 있었다. 이전엔 EMAIL_USE_TLS = True 를 설정값으로 이용했다. TLS와 SSL은 유사하지만 엄연히 다른 프로토콜이었고, 이 값을 EMAIL_USE_SSL로 바꾸니까 정상적으로 작동되었다. 참고로, EMAIL_USE_SSL 과 EMAIL_USE_TLS를 동시에 넣으면 하나만 쓰라고 에러가 난다. 이 부분 반드시 체크하자.

 

 발송 과정은 django.core.mail을 활용하면 되고 워낙 예제가 많아서 생략한다.

  Django에서 기본적으로 제공해주는 로그인 관련 뷰와 템플릿은 매우 편하다. 대략적으론 다음과 같이 사용할 수 있다.

 

yourapp/urls.py

from django.contrib.auth import views as auth_view

urlpatterns = [
    path('login/',
    auth_view.LoginView.as_view(template_name='registration/login.html'), name='login')
    ]

 

templates/registration/login.html

        <div class="alert alert-info">로그인</div>
        <form action="" method="post">
            {% csrf_token %}
            {{form.as_p}}
            <input class="btn btn-primary" type="submit" value="Login">
        </form>

 

 

   그런데, 이 상태에선 기능적으로는 문제가 없지만, 사용자 경험면에서는 문제가 좀 있다.

 

  위의 코드로 만들어진 로그인 화면이고, 위의 Please ~ 문구는 비밀번호를 틀리면 나오는 문구이다.

나는 템플릿엔 어떠한 텍스트도 넣지 않았으니 아마 form에서 자체적으로 산출된 텍스트들일 것으로 추정할 수 있다.

 

  이 글을 보는 사람들은 거의 개발자일 것이니 이 정도 영어는 신경쓰이지 않을 수도 있겠지만,

일반 사용자들은 그렇지 않을 것이다. 그래서, 이 영어 범벅 폼을 개조해서 한글화를 해 보기로 했다.

오류 문구는 물론, Email과 Password 역시 내가 DB에 저장해 놓은 이름이 그대로 출력되고 있기 때문에 이 역시 바꿔보기로 했다.

 


 

  우선, LoginView부터 살펴보도록 하자. 파이참을 사용한다면 해당 클래스에 커서를 대고 Ctrl+B로 해당 클래스의 정의로 바로 넘어갈 수 있다.

 

  밑에 자잘한 함수들이 있으니 이 뷰가 어떻게 동작하는 지 관심이 있다면 읽어보자.

form_class = AuthenticationForm

에만 주목하면 해당 이름의 Form을 로그인에 활용하는 것으로 추정할 수 있고, 밑에서 get_form_class() 라는 함수를 통해 해당 form을 사용하도록 설정하는 코드가 있다.

  그러므로 우리가 수정해야 할 건 이 Form일 것이다.

 

 

 

  아래의 error_messages를 보면 위에서 봤던 익숙한 문구들이 보인다. 이제 이 부분을 전부 수정하면 될 것이다.

수정해야 할 부분을 찾았으니, 이제 코드로 작성해보자.

 

 

 

yourapp/forms.py

from django.contrib.auth.forms import AuthenticationForm

class CustomAuthenticationForm(AuthenticationForm):
    error_messages = {
        'invalid_login': (
            "비밀번호나 이메일이 올바르지 않습니다. 다시 확인해 주세요."
        ),
        'inactive': ("이 계정은 인증되지 않았습니다. 인증을 먼저 진행해 주세요."),
    }

 

 

에러 메세지들은 수정했으니, 이제 필드 이름을 수정할 차례이다. 해당 클래스의 __init__ 함수를 보자.

 

 

 유저네임 필드의 최대 길이와 라벨명 등을 설정하고 있다. 해당 부분을 오버라이딩 하면 될 것이다.

 

 

* 글쓴이는 username 대신 email을 쓰고 있는데? 

 

  맞다. 나는 User 클래스를 아예 새로 만들어서 쓰고 있다. 그래서 로그인에서 ID 없이 이메일과 비밀번호만으로 로그인을 하고 있고, username이라는 필드는 아예 모델에 없다. 그래도 지금까지 위 코드가 정상 작동 했다. 왜냐하면,

self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)

이 부분 때문이다. 장고의 기본 User 모델에 username이라는 필드가 있고 해당 필드를 로그인시에 활용하는 건 맞다.

  일단, 이 Form에서의 username이라는 이름은 그냥 필드 이름 그 이상도 이하도 아니다.

두 이름이 겹치는 건 일단 그냥 우연이라고 생각하면 된다. 중요한 건, 이 Form에서의 username 필드를 정의할 때, Usermodel의 USERNAME_FIELD를 이용해 정의를 한다. 즉 로그인 인증에서 사용하는 유저 모델의 설정을 따라간다.

그리고 나는 커스터마이징한 유저 모델에서 USERNAME_FILED를 email이라고 정의해 두었기에, 맨 위의 로그인 창에서 Email이라고 표시된 것이다.

 


 

위의 관찰 내용을 이용하여 커스터마이징을 계속해 보자.

 

yourapp/forms.py

from django.contrib.auth.forms import AuthenticationForm

class CustomAuthenticationForm(AuthenticationForm):
    error_messages = {
        'invalid_login': (
            "비밀번호나 이메일이 올바르지 않습니다. 다시 확인해 주세요."
        ),
        'inactive': ("이 계정은 인증되지 않았습니다. 인증을 먼저 진행해 주세요."),
    }

    def __init__(self, request=None, *args, **kwargs):
        super(CustomAuthenticationForm, self).__init__(*args, **kwargs) # 꼭 있어야 한다!
        self.fields['username'].label = '이메일'
        self.fields['password'].label = '비밀번호'

 

 

  원한다면 길이 제한 등도 건들 수 있지만 여기서는 필요하지 않아서 따로 수정하지 않았다.

이대로 하기 전에, 다시 한번 원본 코드를 보고 이 부분을 주목해보자.

        if self.fields['username'].label is None:
            self.fields['username'].label = capfirst(self.username_field.verbose_name)

username 필드의 라벨이 지정되어 있지 않으면, 해당 username 필드의 verbose_name을 가져온다고 되어 있다.

그렇다면, 내가 커스터마이징했던 모델에서 verbose_name을 지정한다면 최소한 이메일 부분은 한글화가 되지 않을까?

 

 

class User(AbstractBaseUser):
    email = models.EmailField(
        verbose_name='이메일',
        max_length=255,
        unique=True,
    )
    
    USERNAME_FIELD = 'email'
    # ...

 

이렇게 지정하고, migration을 진행한다면...

 

 

  잘 적용 되었다. 이렇게 된다면 username 필드는 따로 건들 필요가 없게 되었다.

이런 식으로 매번 form class나 템플릿을 만지며 라벨을 일일이 달아 주는 건 비효율적일 것이다. 그래서 직접 form의 label을 수정하는 것보다, 가능하다면 위와 같이 verbose_name을 적용하는게 제일 좋다.

 

 

 

코드를 수정하자.

 

yourapp/forms.py

from django.contrib.auth.forms import AuthenticationForm

class CustomAuthenticationForm(AuthenticationForm):
    error_messages = {
        'invalid_login': (
            "비밀번호나 이메일이 올바르지 않습니다. 다시 확인해 주세요."
        ),
        'inactive': ("이 계정은 인증되지 않았습니다. 인증을 먼저 진행해 주세요."),
    }

    def __init__(self, request=None, *args, **kwargs):
        super(CustomAuthenticationForm, self).__init__(*args, **kwargs) # 꼭 있어야 한다!
        self.fields['password'].label = '비밀번호'

 

 

폼은 완성되었다. 이제 뷰를 수정하자.

 

yourapp/views.py

 

from django.contrib.auth import views as auth_view
from .forms import CustomAuthenticationForm

class CustomLoginView(auth_view.LoginView):
    form_class = CustomAuthenticationForm

 

Form까지 적용이 완료되었다. 이제 뷰를 교체하자.

 

 

yourapp/urls.py

 

from .views import CustomLoginView

urlpatterns = [
    path('login/',
    CustomLoginView.as_view(template_name='registration/login.html'), name='login')
    ]

 

 

이제 결과를 보자.

 

 

  모두 한글화가 되었다. 

맨 아래의 Login 버튼은 Form에 포함된 게 아니라 템플릿에 들어 있는 요소여서 바로 수정할 수 있을 것이다. 

 

 

  이외에도 커스터마이징 할 수 있는 요소가 많이 있다.

여기서 다루지 않은 속성이나 함수들도 적절히 오버라이딩 한다면 훨씬 많은 걸 할 수 있을 것이다.

 

 나는 학교의 현장실습지원 시스템을 통해, 직원수 5명 이내의 조그만 스타트업 기업에서 겨울방학 2달 간 개발 인턴을 수행했다. 회사측에서 주는 돈은 없이, 학교에서 지원하는 인턴 지원금만 받으며 일을 하고, 많은 걸 느꼈는데 그 점들을 조금이나마 공유하고자 한다.

 

 

 일단 회사는 e-커머스나 타사의 광고대행 등을 맡는 기업이고, 설립한지 1년도 채 되지 않은 신생 스타트업이었다. 직원분들 모두 매우 젊은 분들이었고, 휴게실이나 탕비실은 모두 타 회사들과 공유하는 공유 오피스의 형태였다. 사무실은 넓지는 않았지만 꽤 쾌적했다.

 

 

 하는 업무는 웹 페이지에 구글 애널리틱스와 같은 트래픽 분석 툴을 삽입하고, 이 정보들로 소비자들의 행동 패턴을 분석하는 업무... 라고 인턴 공고 때 봤었다. 사실 이런 업무는 말은 거창해 보이지만 실제론 이미 툴이 다 해주기 때문에, 개발보다는 오히려 마케팅 쪽에 가까운 업무이다. 물론 이 점은 지원할 때부터 알고 있었고, 무언가 그럴듯한 개발 업무를 할 만한 자리는 대부분 취업연계형으로 졸업예정자를 뽑고 있던 지라 그냥 현업만 경험해보고 가자는 느낌으로 지원했다.

 

 

 결과는, 구글 애널리틱스의 'ㄱ' 자도 볼 일이 없었다. 회사에서 타 업체가 사용할 웹 사이트 개발 업무를 수주하게 되어, 나와 다른 SW 인턴 두 분과 함께 해당 사이트 개발 프로젝트를 두 달동안 진행하게 되었다. 정확히 두 달 내내 저 프로젝트를 한 건 아니지만, 거의 80% 이상의 업무가 저 프로젝트와 관련되어 있었으므로 이번 인턴의 메인 프로젝트라 할 수 있겠다.

 

 

1. 수평적인 분위기의 회사

 사실 이건 기성 회사들도 회사마다 다른 점인데, 일단 스타트업은 대부분 수평적인 분위기로 알고 있다. 일단 대체적으로 나이대가 높지 않다 보니 개인주의 문화가 발달되어 있는 편이고, 들어오시는 분들도 거의 젊은 분들이다. 직접 다녀보니 그럴 수밖에 없다. 일단 사원이 매우 적다 보니 인원 한명한명이 여러 가지 역할을 맡게 되고, 딱히 실무진과 경영진이라는 구분이 없다. 그냥 일이 닥쳐오는 대로 다 나눠서 해야 하는 입장이다. 그래서 사원들 간의 업무의 괴리감 같은 것도 없다. 대표와 팀장, 인턴이 한 자리에서 일하며 직접 의견 교환을 해야 하는 환경이라, 개인적으론 직함의 구분이 잘 안 느껴졌다. 물론 상호간 모든 의사소통은 당연히 존대를 사용했다.

 

 

2. 팀플 이상의 실무 프로젝트 경험

 

 일단 이 회사가 SW와 조금 관련이 있긴 하지만, IT 전문 회사가 아니었다. 이번 인턴모집을 담당했던 컴퓨터공학과 출신직원분은 계셨지만 사내에서 수행하는 직무는 개발 업무가 아니었다. 결론적으로, 사내에 개발을 담당하는 전문 인력이 없었다. 그래서 따로 멘토 없이 SW인턴들끼리 이슈를 해결해 나가는 과정이 필요했고, 내가 프로젝트의 팀장 역할을 하게 되어 팀원들 간 업무 분담이나 논의 등을 조율하게 되었고, 나중엔 아예 PM 겸 CTO 역할을 했다.

 

 

 개발 업무는 팀 프로젝트와 거의 유사했다. 주어진 과제를 받고, 학생들끼리 해당 과제에 맞춰 프로그램을 작성하고, 그 내용을 고객사에 발표하는 과정이었다. 나도 방학 프로젝트를 진행하는 느낌으로 다녔다. 하지만 팀 프로젝트와 크게 다른 점이 몇 개 있었다.

 

 

 우선, 일반적인 학교 프로젝트는 정해진 주제에 맞춰 어느 정도 고정된 방식으로 해결하는 과정이라면, 회사에서의 개발 업무는 주제만 정해지고 나머지는 전부 자유로웠다. 오히려, 수단과 방법을 가리지 않고 가장 편하게 만드는게 Best였다. 의뢰받은 사이트의 규모가 크지 않은 데다 웹개발 경험이 적은 사람들이 많다는 걸 고려하여, python을 사용하기로 결정했다.

 

 

 사이트를 제작하면서 기술적인 애로사항이나 모호한 부분에 대한 질문, 비효율적이라고 판단되는 사항은 직접 기획자님과 소통하면서 끊임없이 요구사항을 수정해 나갔다. 이 과정에서 필요하다고 생각하면 과감하게 구글 스프레드시트 등의 개발 외적인 툴을 가져와서 사용했다. 

 

 

 두 번째로, PM 역할을 하게 되면서 느낀 것이, 학교 프로젝트와 달리 개발과 관련이 없는 사람들(기획자 등)과 필히 소통을 할 일이 생기는데, 이 사람들에게 우리의 작업을 일상 언어로 표현하는 화법이 필요 했다. 특히, 최종 발표는 우리의 개발 과정에 대한 어떤 정보도 없이 오직 구현 결과물로만 평가하는 사람들이라서, 우리의 작업물을 단순히 표현하는 걸 넘어 멋있게 포장하는 능력이 필요했다. 

 

 

 여기서 가장 도움이 됐던 게 문서였다. 나는 진행하는 프로젝트에 대해 어느 정도 문서를 정리하는 습관이 있는데, 사람들을 설득할 땐 그냥 줄글보단 수치와 표가 훨씬 효과적이었다. 숫자를 증거로 주장이나 설득을 하니 대부분의 사람들이 납득해 주었다. 글 쓰는 실력을 더 키워야겠다고 생각하게 되었다.

 

 

 마지막으로, 내가 개발한 프로그램을 사람들이 직접 쓴다는 점으로 동기부여가 되었다. 팀프로젝트 같은 경우엔 프로젝트가 종료되면 바로 버려지는 일이 흔한 데, 내가 회사에서 작업한 결과물들은 단순히 발표하기 위핸 프로젝트가 아니라 단 한명일지라도 실제로 사용할 사람들이 존재하는 프로젝트들이었다. 그러다 보니, 유지보수나 관리 등을 위해 로그 작성이나 모듈화에 더욱 신경을 쓰고, 무엇보다도  무급으로 일함에도 프로젝트에 애착이 꽤 많이 갔다. 그래서 더욱 신경쓰며 코드를 작성한 것 같다. 특히 모듈화는 굉장히 많이 도움이 됐는데, 회사 특성상 크롤링을 할 일이 많았고 크롤링 프로그램 제작 요구가 몇 건 있었는데 첫 프로그램 개발 때 직접 작성한 모듈을 인턴 프로그램 내내 사용했다.

 

 

3. 실무 경험

 2번이 개발의 의미에서 실무라면, 여기는 그냥 일반적인 의미의 실무이다. 사내에서 slack이나 notion, asana 같은 업무용 툴을 많이 사용했는데, 각 툴마다 특징이 있다. 저런 툴은 학교 생활할땐 사용해 볼 일이 전혀 없는데, 저 툴들을 사용해 본다는 경험 자체가 굉장히 신선했다. 보통 저런 툴은 사용하더라도 특별한 일 없으면 개인용 무료 버젼을 이용하는데, 회사에서는 기능이 추가된 유료 버젼을 주로 사용하기 때문에 여러 신기한 기능들도 많이 사용해 보았다.

 

 

 e-커머스 회사다 보니 회사에서 네이버 스마트스토어, 쿠팡 등의 쇼핑몰에 제품을 올리고 판매하는 일이 주 업무이다. 크롤링을 위해 이런 쇼핑몰 계정에도 조금 손댈 일이 있었는데, 대충 이런 쇼핑몰들이 어떤 식으로 운영되는 지도 이해하게 되었다. 소소하지만 언젠간 이런 지식도 도움이 될 것 같다.

 

 

 사실 위에서 설명한 대로 직급만 인턴이고 월급을 제외하면 정직원과 거의 동일한 처우를 받고, 담당 업무도 정직원과 비슷했다. 이게 개인적으론 가장 재미있는 경험이었다. 프로젝트 수주 회의에서 요구 사항을 기술적으로 검토하며 문제점을 설명하기도 했고, 기획자님에게 직접 어떠어떠한 자료가 필요하다고 지시도 해 보고, 직접 고객사까지 가서 30분 정도 프레젠테이션(하고 조기퇴근...)까지 해 봤었다. 이런 경험은, 인턴 한명한명도 실무에 바로 투입하는 스타트업이라서 가능했었다고 생각한다. 물론 이러한 일을 하는덴 어느 정도 열정과 약간의 지식이 뒷받침되었기에 가능했다.

 

 

 무급 인턴이라 금전적으론 아무런 보상이 없었지만, 이러한 열정과 성과에 힘입어, 며칠간의 재택 근무(!) 등을 얻어내는 등 인턴으로서 생각할 수 없는 많은 혜택을 받았다. 지금 생각해 보니 사실 이만큼 인턴들도 존중해 주는 분위기라서 열심히 일한게 아닐까 하는 생각도 든다. 

 

 

 그리고, 사람들 고생하는 거 보면서 돈 버는 게 새삼 힘든 일이라고 느꼈다... 사실 이게 가장 중요할지도 모른다.

 

 

 

 

 

4. IT 스타트업이 아니라서...

 

  위에서 말했듯이 멘토가 없이 내가 PM의 역할을 했다. 프로젝트 설계도 내 마음대로 할 수 있었고 실제로 90% 이상의 프로젝트 설계가 내가 생각한 대로 이루어졌다.

 

 

 하지만, 이렇게 진행하면서 무언가 막힐 때마다 항상 "내가 어딘가 잘못한 걸까?" 라는 생각이 끊임없이 머릿속을 맴돌았다. 무슨 에러가 난다는 구글에 검색할 수 있지만, '내가 생각한 이 방법이 옳은가? 더 나은 프로세스는 없는가?' 라는 질문엔 어디서도 답을 찾을 수가 없었다. 멘토가 없다는 점이 여기서 가장 큰 체감이 되었다. 심지어 내가 짜는 코드가 옳은 지도 확신이 안 들었다. 혹시 내가 잘못된 방식으로 매일 코드를 짜다가, 그 방식이 습관이 되어 버리는게 아닐까 하는 불안감도 있었다. 개발 전문 회사가 아니다 보니 딱히 사내 가이드라인 같은 것도 없었고, 오히려 내가 가이드라인을 작성해야 하는 입장이었다.

 

 

 그래도 끊임없이 고민하면서, 최대한 비슷하게 구글에다가도 검색해 보고, 가끔은 아는 선배분들께 조언도 구해보면서 나름대로 최선의 길을 찾으려 노력했다.  

 

 

  또, 개발 관련 회사가 아니기에 당연하겠지만, 처음 회사에 들어왔을 땐 사내에 개발 관련한 어떤 가이드라인도 없었다. 심지어 협업을 할 수 있는 버젼관리 툴도 없었고, 회사에서 사용하는 AWS EC2 인스턴스도 이전 여름 인턴분들이 하고 가신 상태로 방치되어 있었다. 참고로 저 EC2 인스턴스 같은 경우엔 비밀 키마저 잃어버려서 접속이 안 됐고, 반나절동안 비밀 키를 초기화하는 작업을 수행했었다. 그래서 처음 프로젝트를 요청받았을 때, 우선 회사의 github 계정을 만들었고, 그 계정으로 private 레포지토리를 생성하는 가이드라인을 작성하는 게 시작이었다.

 

 

 이렇게 IT 관련 인프라가 전무하다 보니, 프로젝트를 진행할 때 나쁜 의미로 학교 팀플처럼 진행되었다. 새로운 기술들을 사용할 일 없이, 두 달 내내 python과 git이라는 익숙한 기술들만 활용해서 작업하게 되었다. 체계적인 모듈을 활용한 테스트나 상호간 코드 리뷰같은건 엄두도 못 냈다. 물론 내가 이런 걸 하자고 제안하기엔 시간적, 인적 제약도 있고 인턴이라는 위치상 곤란했다. 물론, 이런 것 없이도 프로젝트가 잘 마무리 되었지만, 개발 과정 자체는 그냥 학교 프로젝트를 하나 더 한 느낌에 가까웠다. 무언가 실무 테크닉이나 기술, 업무 문화를 경험해보고자 했던 나한텐 가장 아쉬운 점이었다.

 

 

 

 

 

5. 결론

 

 위 내용을 요약하자면, 다음과 같다.

 

+ 인턴보다는 정직원에 가까운 실무 경험

+ 수평적인 분위기

 

? 다양한 경험. (순수하게 기술만 경험하고 싶은 사람한테는 단점이 될 수도 있다.)

 

- 사내 기술/인프라의 부족

 

 개인적으론 매우 뜻깊고 꽤 재미있는 경험이었다. 회사 사람들도 매우 좋았고 좋은 추억만 남았다. 그렇지만, 여기서 계속 일하고 싶냐고 말하면 대답은 Yes는 아닐 것이다. 나는 아직 초보자이고, 아직 많은 걸 배워야 할 사람이라고 생각하기에 좀더 뛰어난 사람들 밑에서 새로운 걸 배울 수 있는 환경이 좋을 것 같다. 그리고 반드시 SW 관련 회사에 들어가는 게 내 미래를 위해 좋을 것 같다.

 

 

 

 

 

 

 1. 사용 배경

  기술적인 구현 내용을 알고 싶다면 바로 2번으로 건너뛰면 된다. 

 

 인턴으로 다니던 회사에서 데이터를 주기적으로 크롤링하고, 크롤링한 자료를 이용자들에게 제공해주는 웹 서버를 개발했다. 이 서버를 운영해야 하는데, 이 모든 작업이 정상 작동하는지 주기적으로 확인을 해야 한다. 서버는 사실 바로 안다. 그냥 주소를 입력했는데 안 들어가지면 문제가 있다. 내가 확인하기도 전에 아마 사용자 측에서 뭔가 피드백이 들어올 것이다.

 

 하지만, 크롤링이 잘못되었을 경우엔 이 크롤링이 잘못되었다는 사실을 깨닫는 건 상대적으로 까다롭다. 그냥 데이터가 몇 개 비거나, 혹은 아예 안 보일 뿐이지 다른 모든 건 정상작동 할 것이다. (서버와 크롤링 모듈을 합쳐놓지 않은 이상) 물론, 서버 로그를 확인하는 방법도 있다. 하지만 AWS를 사용하는 지금으로선 putty를 켜고 들어가는 거 자체도 너무 귀찮았다. 그래서 그냥 이 과정을 자동화 시켜서, 나는 그냥 앉아서 정상 작동 여부를 알도록 했다.

 

 

 

 해당 크롤링이 성공이든, 실패든 일단 무조건 메세지를 보내서 성공/실패를 보고하도록 하고 싶었다. 일단 언어는 크롤링 모듈이 python으로 만들어 졌기에 python으로 고정하고, 이제 이 메세지를 보낼 매체를 선정하는데, 선정 결과는 다음과 같았다.

 

- 이메일 : 프로토콜도 있고 다 있는데, 이미 내 네이버 메일함은 999+라 메일 공해를 더 일으키고 싶진 않다.  그렇다고  업무용 메일을 또 만들기엔 솔직히 내가 확인을 자주 안 할거같다.

 

- 카카오톡 : 접근성도 최고고, 다 좋은데 사적 메신저를 업무용으로 쓰고 싶진 않았다. 결정적으로, 현재 메세지 보내는 API 서비스가 종료하고 새로운 챗봇 서비스를 준비하는 모양인데, 이게 2020년 2월 기준으로 베타 테스트 중이라 사유를 적어서 신청해야 쓸 수 있다.

 

- 텔레그램 : telegram이라는 모듈이 있어서 코딩하기엔 편리한데, 이것도 사실 업무용보다는 사적인 메신저라 업무용으로 쓰고 싶진 않았다.

 

- Slack : slacker라는 쓰기 쉬운 모듈도 있고, 원래 업무용으로 나온 메신저다. 

 

 그래서 Slack bot을 만들기로 했다.

 

 

2. Slack bot 추가하기

 

 일단 당연히 Slack 워크스페이스를 만들어야 한다. 다행히 만드는 건 무료다. 무료 기능은 검색 기능이 15000 메세지로 제한되는 점을 빼면 개인적으로 사용하는 수준에선 딱히 지장이 없을 것이다. 사실 실무에서 slack을 사용하고 있었다면 이미 유료 에디션일 것이다.

 

 

 일단, 봇은 여기서 app이라고 부르는 데 이 app을 만들어야 한다. 

https://api.slack.com/ 여기로 본인 아이디로 로그인하고, 화면 중앙의 Start building -> Create New App을 클릭한다.

 

 

 

본인이 쓸 앱 이름과 이 앱이 적용될 워크스페이스를 지정한다. 아까 만든 워크스페이스를 지정하면 될 것이다.

 

 

 

 이제 앱은 생성되었고, 설정을 할 시간이다. 보면 bots와 같이 사용자와 인터렉션을 할 수 있도록 다양한 옵션이 준비되어 있는데, 지금 만드는 것은 양방향 소통이 아니라 일방적으로 정보를 전송해 주는 구독형 봇이라 Permissions만 손대보자.

 

 

  

 

 

 

들어가면 우선 Install App to Workspace라고 이름만 보면 반드시 눌러야 할 거 같이 생겼는데 안 눌리는 버튼이 있다. 당황하지 말고 지금은 권한이 아무것도 지정이 안 되어서 설치를 못 하는 것이다. 스크롤을 내리자.

 

 

 

 

밑에 Scopes에서, 우리는 Bot으로 쓰는 거니 Bot Token Scope에서 지정해보자. 여러 가지 권한들이 있는데, 내가 사용할 것은 chat:write 다. chat:write는 당연히 글을 써야 하니 주는 기능이다. 클릭하면 바로 적용이 된다.

 

 

 이외에도 사용 가능 IP 제한 설정이나 리디렉션 설정 등이 있다. 필요한 설정이 있다면 하면 되고, 이제 다시 보면...

 

 

 

 이제야 인스톨 앱 버튼이 활성화가 됐다. 다음엔 이런 권한을 준다고 확인창이 뜨는데, 읽어보고 자기가 준 권한이 맞다면 승인을 누르자.

 

 

 

 

 

 Bot 토큰을 받는데, 이 토큰으로 봇을 조종하니 유출되지 않게 하자.

그리고 이 봇이 들어갈 채널을 만들자. 테스트인데 그냥 #test면 심심하니까 #testtest로 했다.

 

 

 

눌러야 하는 버튼이 보인다. Add an app을 누르자.

 

 

 

 아까 만든 TestService가 보인다. Add를 하면 채널에 초대가 된다.

그런데, 처음 만들 땐 Add가 아니라 그냥 View로 보이면서 초대가 안 될 때가 있다. 이 경우는 그냥 기다리니까 다 해결이 됐는데, 내 추측으로는 앱이 만들어졌다는 사실 자체는 바로 전송되는데, 앱이 해당 워크스페이스에 적용되는 데는 시간이 걸리는 것 같다. 

 

 

 

 

초대가 됐다. 참고로 아이콘이나 이름을 편집하고 싶다면, 앱 관리 페이지의 Basic Information에서 이름이나 아이콘 등을 바꿀 수 있다.

 

 

 이걸로 모든 준비가 끝났다.

 

 

 

3. Python 코드 작성

 

 pip install slacker를 통해 라이브러리를 설치한다.

 

사용은 놀랍도록 간단하다.

 

# Slackbot.py

from slacker import Slacker
from datetime import datetime

token = '아까 받은 토큰을 이곳에 삽입'


def send_slack_message(msg, error=''):
    full_msg = msg
    if error:
        full_msg = msg + '\n에러 내용:\n' + str(error)
    today = datetime.now().strftime('%Y-%m-%d %H:%M:%S ')
    slack = Slacker(token)
    slack.chat.post_message('#testtest', today + full_msg, as_user=True)

if __name__ == "__main__":
    # 일부러 에러가 발생하도록 함.
    try:
        a = 1/0
    except Exception as e:
        send_slack_message('테스트', e)

 사실 메세지 전송 자체는 딱 import 한 줄, token 한 줄, slacker 두 줄로 총 네 줄이면 충분한데, 나는 해당 에러 발생 시간도 알기 위해 시간 정보 역시 삽입했다. 아래의 __main__ 코드를 통해 에러를 발생시켜보자.

 

 

 다음과 같이 메세지가 보내어진다. 기본적으론 이게 끝이다!

 

4. 프로젝트에서의 활용

 

 이번엔 내가 사용했던 용례를 소개하고자 한다. 우선, 가장 기본적으론 이렇게 사용할 수 있겠다.

 

def run():
    # 여기에서 크롤링 진행


if __name__ == "__main__":
    try:
        run()
        send_slack_message('크롤링이 완료되었습니다.')
    except Exception as e:
        send_slack_message('크롤링 작업 중 오류가 발생했습니다!', e)

 

 이러면 run()이 오류 없이 진행되었을 시, 크롤링이 완료되었습니다 라는 메세지가 보내지고, 에러가 나면 에러내용과 함께 에러가 발생했다는 메세지가 전송된다. 성공을 하든 실패를 하든 일단 메세지를 보내게 된다. 만약 정해진 시간에 아무런 메세지도 안 온다? 이러면 아예 실행 자체가 안 된 것이니 역시 서버를 체크하면 된다. 성공의 경우도 메세지를 보내야 하는 이유가, 서버가 OS 다운 등의 모종의 사유로 아예 스크립트를 못 돌리는 상황도 체크하기 위함이다.

 

 

 

 하지만 이 경우는 단순히 크롤러의 성공/실패 여부만 따질 뿐, 문제가 있다. 말 그대로 프로그램을 돌릴 수 없는 '에러' 만 잡는 상황이고, 의도하지 않는 값이 나오는 경우는 잡을 수 없다. 즉 함수에 들어가야 할 값이 안 들어가고 None이 들어가서 None이 나오는 경우, 우리가 의도한 상황이 아니지만 아무튼 에러는 안 난다. 이런 경우를 위해, 사용자 정의 에러를 사용하자. 

 

from Slackbot import send_slack_message

class LoginError(Exception):
    def __init__(self, target_name):
        self.msg = '{target} 로그인 양식이 맞지 않습니다.'.format(target=target_name)
        send_slack_message(self.msg)
    def __str__(self):
        return self.msg


class DBError(Exception):
    def __init__(self, name, length, error):
        self.msg = '{name} 에 대한 {len} 행의 데이터 입력 작업 중 오류가 발생했습니다.\n{error}'\
            .format(name=name, error=error, len=length)
        send_slack_message(self.msg)
    def __str__(self):
        return self.msg

 

 코드 내에서 값의 유효성을 검증하는 부분을 추가하고, 유효하지 않을 경우 위와 같은 에러들을 raise 한다. 이 에러들은 raise 됨과 동시에 __init__ 에 의해 슬랙으로 에러 내용에 대해 정해진 양식의 메세지를 보내게 된다. 나는 실제 코드에선 Slack API가 작동하지 않을 것을 대비하여, 위의 코드에선 표시하지 않았지만 logging 모듈을 이용해 로컬 파일로 에러 로그를 작성하는 부분도 매 에러마다 추가해 두었다. 

 

 

 

 이외에도, 메세지는 보내되 크롤링은 그대로 진행해야 하는 경우도 있다. 1000개 중 하나만 오류가 난다면, 그 하나 때문에 크롤링이 완전히 중단되는 것보단 999개는 진행하고 1개는 나중에 처리하는 것이 합리적일 것이다. 그런 경우에도 간단히 이런 식으로 작성하면 될 것이다.

 

for name in name_list:
    try:
        crawl_something(name)
    except:
        send_slack_message('크롤링 중 {name} 를 처리하는 데 오류가 발생했습니다.'.format(name=name))

 개인적으론 이 방식은 항목 하나라도 크롤링이 안되면 치명적이거나, 데이터 수가 매우 적은 경우를 제외하곤 추천하지 않는다. 천개짜리 name_list를 크롤링을 진행하는데 모두 오류가 나서 천 개의 메세지가 왔다고 가정하면... 끔찍하다. 차라리 실패한 갯수(except 블록이 실행된 횟수)를 카운팅해서 그 갯수를 보내주는 게 합리적일 것이다. 내 경우엔 일단 몇 개 정도는 당장 크롤링에 실패해도 서비스에 큰 지장은 없어서, except 블록에서 해당 에러를 로그 파일에 기록만 하도록 작성해 두었다.

 

 

 

5. 주의사항

 위의 방법은 오류가 났다는 걸 능동적으로 알려주기만 할 뿐, 절대 오류를 해결해 주는 과정이 아님을 명심하자. 따라서 위에서는 가독성을 위해 생략했지만 문제 원인 파악과 해결을 위해선 logging 등의 기본적인 로그 작성은 필수다. 그리고, 유효성 검증을 적절하게 할 수 있도록 코드를 짜는 것 역시 중요하다.

 

 

 

 

 

 현재 모 스타트업 기업에서 인턴을 하고 있다. 이곳에서, 기업들의 각종 데이터를 지정해둔 사이트들에서 크롤링해서 사용자에게 보여 주는 웹 서비스를 제작하고 있다. 중요한 건, 크롤링에 필요한 정보들이 수시로 변해서, 이 정보를 유동적으로 수정하고, 삭제하고, 지정할 수 있어야 했다. 처음에는 데이터베이스를 통해 이 정보를 취급하려고 했었다.

 하지만, 이렇게 하기엔 문제가 있었다.

 

 

1. 이 서비스가 운영될 환경엔 전문 개발자가 없다. 따라서 DB에 대한 지식이 없는 관리자가 쉽게 데이터에 접근하고 수정할 수 있어야 하고, 그런 의미에서 DBMS를 통해 접속해서 데이터를 관리하는 건 부적절하다.

 

2. 그럼, 미리 데이터에 대한 질의나 수정 등 가능한 명령사항들을 래핑해서 일반적인 관리자들(DB에 대한 지식이 없는)이 이용 가능한 서비스로 만들 수도 있다. 하지만, 이 방법은 서비스의 규모나 가용 자원에 비해 너무 큰 작업이라고 생각했다.

 

 

 이러한 사유로 DB를 사용하는 건 포기했다. 대신, 나는 다른 점을 주목했다.

 

1. 여러 가지 정보를 크롤링하는데, 그 중 가장 자주 일어나는 크롤링도 겨우 일 1회이다. 데이터 특성상 실시간으로 모을 필요는 없었다.

 

2. 기업의 정보는 생각보다 자주 바뀐다. 따라서 수정 작업이 빈번할 것이다. 또한, 단순히 크롤링을 위한 URL 데이터 뿐만 아니라, 기업에 대한 다른 데이터들도 저장하고 있었고, 이 데이터들은 정해진 형식이 있는 경우가 많다.

 

 

 

 그래서, 나는 마땅한 대안을 찾다가 구글 스프레드시트를 주목했고, 읽기/쓰기 관련 API도 있다는 걸 알게 되고 데이터 저장을 위해 구글 스프레드시트를 사용하기로 결정했다. 

 

 

 혹시 오해할까봐 미리 적자면, '크롤링 할 대상에 대한 정보'를 구글 스프레드시트에 저장하는 것이고, '크롤링 한 결과'는 최소 수만 단위니까 당연히 DB에 저장했다. 구글 스프레드시트 입출력이 당연히 DBMS보다는 CRUD 관련 성능이 좋지 않다. 당장 사내에서 사용하는 시트 15개 X 시트당 약 5천행씩 존재하는 스프레드시트도 열 때마다 로딩을 몇초씩 기다려야 한다. 그러니까, 아래 방법은 데이터가 수백~수천 개 단위로, 비교적 크지 않을 떄만 활용할 수 있는 방법이다.

 

 

 

1. 스프레드시트 준비

 

 이 부분은 따로 설명이 필요 없을 것 같다. 다만, 공개 데이터가 아니니까 당연히 비공개 스프레드시트로 하고 권한도 적절히 설정한다. 이 부분이 스프레드시트를 사용할 때 큰 장점중 하나라고 생각한다. 사용자에게 수정/열람/공개 권한 등을 손쉽게 설정할 수 있다. 

 

 

2. API 사용 준비

 

 

 일단 결론부터 미리 설명하자면, 이런식으로 스프레드시트 공유자에 봇이 추가되도록 하는 게 목표다. 그러려면 우선 구글 API를 사용할 수 있도록 준비해야 하는데, https://console.developers.google.com/apis에 들어간다.

 

 

 

 나는 현재 프로젝트가 하나 있기에, 지금 보이는 화면과 조금 다를텐데, 맨 위의 google APIs 옆에 아마 '프로젝트 없음'으로 뜰 것이다. 그걸 누르면 사진과 같이 프로젝트 선택이 뜨는데, 나는 만들어 뒀던 게 있어서 기존 프로젝트가 뜨지만, 아마 아무 것도 없을 것이다. 이제 새 프로젝트를 눌러서 프로젝트를 만들면 된다. 이 이후엔 읽어보고 특별한 사항이 없다 싶으면 예스맨 하면 된다.

 

 

 

 

 

 이제 프로젝트가 만들어졌고, 구글 스프레드시트 API를 사용하도록 설정해야 한다. 하라는 대로 API 및 서비스 사용 설정을 누르자. 그러면 크롬 웹스토어랑 비슷한 화면이 뜰 텐데 검색창에 sheet만 쳐도 아래와 같은 API가 나올 것이다.

 

 

 

 

 

 사용 설정 버튼을 누르면 된다.

 

 

 그러면 이 화면이 뜰 텐데, 아직 이 상태론 사용할 수 없다. 내가 누구인지 확인하기 위해 사용자 인증 정보를 만들어야 한다. 친절하게 만들라고 위에 버튼까지 띄워줬으니 만들러 가자.

 

 

 

 

다음과 같은 화면이 뜰 텐데, 내가 썼던 방법을 소개하자면 나머지는 무시하고 위에서 '서비스 계정' 링크를 클릭한다.

 

 

 

 

서비스 계정이 당연히 없을 것이다. '서비스 계정 만들기'를 클릭한다.

 

 

 

원하는 대로 서비스 계정 이름과 설명을 넣는다. ID는 자동으로 만들어진다.

 

 

 

 

계정 권한은 다음과 같이 준다. '프로젝트-편집자'를 택했다. 사실 이 부분이 효과가 있는지는 모르겠는데, 혹시 몰라서 일단 줬다. 읽기 권한만 필요하면 뷰어만 줘도 될 것이다. (요 부분은 정확히 아시는분이 있으면 댓글 부탁드리겠습니다..)

 

 

 

 마지막으로 밑의 '키 만들기'를 누른다. JSON 형식으로 저장하면 된다. 이 키를 이용하여, 스프레드시트에 엑세스 할 것이다. 매우 중요한 파일이고 복구가 되지 않으므로 분실하지 않도록 함은 물론이고, 유출되지도 않아야 한다. 그래서 git을 사용해 관리할 땐, private 레포가 아니라면 꼭 이 파일은 ignore 해주자. 처음 생성하면 이름이 매우 길고 쓰기 힘드므로 적당히 UserKey.json 정도의 이름으로 바꿔주자.

 

 

 마지막으로, 스프레드시트에 해당 서비스 계정의 이메일을 스프레드시트에 공유 설정하자. 권한은 해당 봇이 사용할 만큼 주면 된다. 해당 이메일은 서비스 계정 탭에서도 볼 수 있고, 잘 모르겠다면 방금 받은 json 파일의 'client-email' 항목에 있다.

 

 

 

 이제 사용 준비가 완료되었다.

 

import gspread, os
from oauth2client.service_account import ServiceAccountCredentials

# 구글 API 사용을 위한 상수들
scope = ['https://spreadsheets.google.com/feeds',
         'https://www.googleapis.com/auth/drive']
key_file_name = 'UserKey.json' # 아까 받은 json 인증키 파일 경로가 들어가면 됨.

credentials = ServiceAccountCredentials.from_json_keyfile_name(key_file_name, scope)
spreadsheet = gspread.authorize(credentials).open("<스프레드시트 이름>")

pip install gspread, oauth2client 를 통해 필요한 라이브러리를 설치한다.

이제 spreadsheet를 통해 원하는 대로 조작하면 된다.

 

 

3. API 활용하기

 

 스프레드시트 API에 대해서는 구글 API 문서나 다른 블로거분들의 훌륭한 글이 많아서 따로 언급하진 않지만, 사용에 주의사항이 있다.

 

 

 구글 Sheet API 무료 이용 한도이다. 기본적으로는 무제한이지만, 개인당 매 100초당 100회, 즉 초당 1개씩꼴의 요청만 보낼 수 있다. 즉, loop문 속에서 셀 하나씩 가져오는 식으로 코딩하면 바로 한도에 걸린다. 따라서, 다음과 같은 식으로 코딩해야 한다.

 

    worksheet = spreadsheet.worksheet('<해당 시트 이름>')
    
    product_url_list = worksheet.col_values(3)
    product_code_list = worksheet.col_values(4)

    product_url_list = product_url_list[2:-1] # 필요한 만큼 가공
    product_code_list = product_code_list[2:-1] # 필요한 만큼 가공

    return list(zip(product_url_list, product_code_list))

 

 

 내가 사용했던 시트의 경우 단순화하면 이런 내용이었다.

데이터 목록
코드 바코드
81048 810483
82069 820697
... ...
합계 데이터 행

 나는 코드와 바코드 데이터만 필요하고, 데이터 목록이나 합계 같은건 필요없었다.  col_values(n) 같은 경우엔 해당하는 n열의 데이터를 모두 가져온다. 따라서 요청 한번으로 해당 열의 정보를 모두 가져오고, slicing을 통해 필요없는 데이터를 쳐냈다. 위의 경우엔 앞의 2행과 마지막 1행이 필요가 없으니 [2:-1] 을 통해 간단하게 가공했다. 지금 같은 경우는 간단한 경우고, 복잡한 처리가 필요할 땐 slicing하는 부분에 함수를 각각 따로 만들어서 처리하는 부분을 만들면 된다. 마지막엔 열 단위로 나눈 데이터를 다시 묶었다. 위의 경우엔 [('81048', '810483'), ('82069','820697'), ...] 과 같은 식으로 반환될 것이다.

 

 

 

4. 스프레드시트 사용의 장점 및 단점

 

 위에서도 언급했다시피 데이터 수정과 열람, 삭제가 자유로움은 물론, 데이터 형식을 강제하여 규칙성을 유지할 수 있고, 필요한 최소한의 열만을 입력할 수 있다. 한 가지 내가 겪은 사례를 들자면, 나는 어떤 열에 숫자가 들어가되, 무조건 6자리로 들어가도록 해야 한다. 13이 들어가도 000013 이런식으로 들어가야 한다. 이것을 스프레드시트에서 서식 -> 숫자 -> 추가 형식에서 직접 지정을 했다.

위와 같이 지정하면 그냥 13을 입력해도 000013 과 같은 식으로 자동으로 0을 채워준다.

 

 

 엑셀 함수 역시 활용할 수 있는데, 예를 들어 이 함수를 통해 해당 셀을 참고하여, 옆 셀이 비었으면 '비상장', 빈 셀이 아니라면 '상장사'를 자동으로 입력한다. 이런 식으로 함수를 활용하면 사용자가 직접 타이핑할 데이터를 줄일 수 있다. 사용자의 부담이 줄어듬은 물론이고, 예기치 못한 오타 역시 방지할 수 있다. 이외에도 특정 열에 중복을 허용하지 않도록 함으로서, 기본 키의 역할을 하도록 할 수도 있다.

 

 

 

 다만, 이러한 스프레드시트를 통한 DB의 대체는, 데이터 읽기가 겨우 일 1회만 일어날 정도로 드물고, 데이터의 양이 수천 행 정도로 비교적 적었기에 가능했다. 실제로 데이터의 실시간 반영에 대해서도 많이 고민했는데, 결국 스프레드시트의 한계를 인정하고, 새벽 시간에만 데이터를 한꺼번에 받아와서 읽도록 했다. 

 

 

 최종적으로 완성한 시스템의 구조는, 스프레드시트의 데이터를 새벽 2시에 한꺼번에 읽어와서 서버의 DB에 기존 테이블을 삭제한 후 새로 받은 데이터를 넣고(이 과정이 행 500개 기준으로 10초 조금 안되게 걸린다.), 그 데이터를 기반으로 크롤링을 하도록 했다. 기존 테이블을 삭제하는 이유는, 엑셀에서 어떤 데이터가 변경되거나 삭제되었는지 아는 방법이 까다롭고, 매 행을 대조하는 식으로 억지로 구현할 바에는 그냥 매번 데이터를 새로 받아 쓰는 게 차라리 더 편했기 때문이다. 큰 데이터에 적합하지 않은 이유는 이러한 까닭도 있다.

 

 

 마지막으로, 사용자가 엑셀에 대해 잘 모르면 효율이 떨어질 것이다. 함수나 서식 지정 같은 요소를 모르면 위처럼 데이터의 형식성을 강제할 수 없고, 결국 타이핑하는 사람에게 맡겨야 할 수밖에 없으니..

 

 

5. 결론

 

 사실 처음 이 프로젝트에 대해 들었을 땐 이런 방법을 쓰리라곤 상상도 못했다. 사실 이 방법이 다루는 데이터가 많아야 수만~수십만 개 수준인 소규모 프로젝트라 가능했겠지만, 그래도 꽤나 재미있는 발상으로 극복해 낸 것 같다. 그래도 스프레드시트와 DB를 동시에 사용하면서, DB의 장점과 단점을 동시에 경험해 볼 수 있는 기회였다.

+ Recent posts