Django에 Social Login 붙이기: Django세팅부터 Facebook/Google 개발 설정까지

Django등 웹 서비스를 제공하며 항상 다루게 되는 주제가 있습니다. 유저를 우리 서비스의 유저 모델을 통해 직접 가입시키느냐, 혹은 타사의 oAuth를 이용한 Social Login을 붙여 가입없이(혹은 최소화) 서비스를 이용할 수 있도록 유도 하느냐 등입니다.

Django에서 이러한 Social Login을 이용하기 위한 라이브러리는 여러개가 있었고, 대표적으로는 django-social-authpython-social-auth가 있었지만, 두 프로젝트 모두 현재(2017.02.08기준) Deprecated되었고 이 프로젝트들은 python-social-auth가 org자체로 이전해 social-auth-app-django로 바뀌었습니다.

한편 -social-auth들의 대체재로 django-allauth가 있는데, 올해 1월에도 새 버전 릴리즈가 있는만큼 활동적인 프로젝트입니다. (하지만 이번 글에서는 다루지 않습니다.)

이번 게시글에서는 social-auth-app-django을 이용해 Django 프로젝트에 social login을 붙여봅니다.

참조한 공식 docs는 python-social-auth configuration django에서 볼 수 있습니다.

참고: social-auth-app-django는 pip패키지 이름이며, 프로젝트 이름은 python-social-auth로 동일합니다.

설치하기

widgets:

1
pip install social-auth-app-django

Django의 기본 ORM을 이용하고 있다면 social-auth-app-django를 pip로 설치하면 됩니다.

settings.py 설정하기

widgets:

INSTALLED_APPS 추가하기

settings.py에 social_django를 추가해줍니다.

1
2
3
4
5
INSTALLED_APPS = (
...
'social_django',
...
)

앱 추가후 migrate를 해줘야 정상적으로 Social Auth용 DB Table이 생성됩니다.

1
python manage.py migrate

AUTHENTICATION_BACKENDS 추가하기

Social Login은 기존 유저모델과 함께 사용이 가능합니다. 하지만 기본 유저 ModelBackend를 사용하지 않고 독자적인 ModelBackend를 사용하기 때문에 settings.py의 AUTHENTICATION_BACKENDS에 Social login용 Backends를 추가해줘야 합니다.

1
2
3
4
5
6
AUTHENTICATION_BACKENDS = [
'social_core.backends.google.GoogleOAuth2', # Google
'social_core.backends.facebook.FacebookOAuth2', # Facebook
...
'django.contrib.auth.backends.ModelBackend', # Django 기본 유저모델
]

django.contrib.auth.backends.ModelBackend가 있어야 createsuperuser로 만들어진 계정의 로그인이 가능해집니다.

Social Login용 URL Namespace 지정

최상위 프로젝트 urls.py에 지정할 social login의 namespace를 지정해줍니다. 또한, Login 후 어떤 URL로 장고가 유저를 Redirect시킬지 지정해 줍니다.

1
2
3
SOCIAL_AUTH_URL_NAMESPACE = 'social'

LOGIN_REDIRECT_URL='/'

꼭 namespace가 ‘social’일 필요는 없습니다. 하지만 가이드에서는 ‘social’을 사용하기에 아래 urls.py 설정에서도 동일하게 사용할 예정입니다.

Social Login을 위한 API Key/Secret 설정하기

우선 프로젝트 BASE_DIR(manage.py파일이 있는 폴더)에 envs.json이라는 이름의 환경변수를 담은 json 파일을 만들어 줍니다.

1
2
3
4
5
6
{
"FACEBOOK_KEY":"숫자숫자숫자들",
"FACEBOOK_SECRET":"숫자영어숫자영어들",
"GOOGLE_KEY":"숫자-영어.apps.googleusercontent.com",
"GOOGLE_SECRET":"숫자영어대문자들"
}

당연하게도 위 파일은 실제 동작하는 Key와 Secret이 아닙니다.

Social Login을 사용하기 위해 Google에서는 Google+ API를 활성화 하고 OAuth 2.0 클라이언트 ID를 ‘웹 애플리케이션’으로 생성해 API Key/Secret을 발급받아야 합니다.

Google Login은 Google+ API에 연결되어있기 때문에 다른 Login API는 없습니다.

Facebook의 경우에는 Facebook for Developers에서 새 앱 추가 후 ‘Facebook 로그인’ 제품을 활성화 시킨 후 앱의 대시보드에서 앱 ID와 앱 시크릿 코드를 받아 이용하면 됩니다.

두 서비스 모두 지정된 url에서만 동작하기 때문에 Google의 경우에는 ‘OAuth 2.0 클라이언트 ID’에서 ‘승인된 리디렉션 URI’에 http://localhost:8000/complete/google-oauth2/을 추가해줘야 하며, Facebook의 경우에는 ‘Facebook 로그인’의 ‘클라이언트 OAuth 설정’에 있는 ‘유효한 OAuth 리디렉션 URI’에 http://localhost:8000/을 추가해주면 됩니다.

위 설정을 모두 하지 않을 경우 40x번대 에러가 발생합니다.

이제 위에서 만든 envs.json파일을 환경변수로 사용해야 합니다. settings.py파일 최상위에 이와 같은 코드를 적용해 줄 경우, 개발용 envs_dev.json와 배포용 envs.json, 그리고 환경변수로 관리되는 경우 모두 커버가 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import os
import json

from django.core.exceptions import ImproperlyConfigured

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Env for dev / deploy
def get_env(setting, envs):
try:
return envs[setting]
except KeyError:
error_msg = "You SHOULD set {} environ".format(setting)
raise ImproperlyConfigured(error_msg)

DEV_ENVS = os.path.join(BASE_DIR, "envs_dev.json")
DEPLOY_ENVS = os.path.join(BASE_DIR, "envs.json")

if os.path.exists(DEV_ENVS): # Develop Env
env_file = open(DEV_ENVS)
elif os.path.exists(DEPLOY_ENVS): # Deploy Env
env_file = open(DEPLOY_ENVS)
else:
env_file = None

if env_file is None: # System environ
try:
FACEBOOK_KEY = os.environ['FACEBOOK_KEY']
FACEBOOK_SECRET = os.environ['FACEBOOK_SECRET']
GOOGLE_KEY = os.environ['GOOGLE_KEY']
GOOGLE_SECRET = os.environ['GOOGLE_SECRET']
except KeyError as error_msg:
raise ImproperlyConfigured(error_msg)
else: # JSON env
envs = json.loads(env_file.read())
FACEBOOK_KEY = get_env('FACEBOOK_KEY', envs)
FACEBOOK_SECRET = get_env('FACEBOOK_SECRET', envs)
GOOGLE_KEY = get_env('GOOGLE_KEY', envs)
GOOGLE_SECRET = get_env('GOOGLE_SECRET', envs)

이와 같이 사용할 경우, APACHE웹서버 등에서 시스템 환경변수를 불러오지 못하는 상황이거나, HEROKU나 PythonAnywhere와 같은 PaaS에서도 Django코드와 API키들을 완전히 분리해 사용할 수 있습니다.

위에서 지정한 FACEBOOK_KEY들을 SocialLogin에 할당해 줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
# SocialLogin: Facebook
SOCIAL_AUTH_FACEBOOK_KEY = FACEBOOK_KEY
SOCIAL_AUTH_FACEBOOK_SECRET = FACEBOOK_SECRET
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id, name, email'
}

# SocialLogin: Google
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = GOOGLE_KEY
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = GOOGLE_SECRET
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['email']

위 코드는 가장 기본적인 ‘email’을 유저 식별도구로 받아옵니다.

_PROFILE_EXTRA_PARAMS를 이용해 다른 Field를 받아올 수도 있습니다. (필수 아님)

project폴더의 urls.py 설정하기(최상위 urls.py)

widgets:

이제 프로젝트 폴더의 urls.py에 Social Login이 사용할 url들을 등록하고 namespace를 지정해 Template에서 사용할 수 있도록 설정해야 합니다.

1
2
3
4
5
6
7
8
from django.conf.urls import url, include # url뿐 아니라 include를 import해야 합니다.
from django.conf import settings
from django.contrib import admin

urlpatterns = [
url(r'^admin/', admin.site.urls),
url('', include('social_django.urls', namespace='social')), # 이 줄을 등록해주면 됩니다.
]

이와 같이 social_django.urls를 include하고 ‘social’ namespace를 등록해 줍니다.

Template에서 Social Login url 호출하기

widgets:

위 코드들을 추가해주는 것 만으로도 기본적인 Social Login기능은 완성되었습니다. 이제 Template에서 호출을 해봅시다.

1
2
{% raw %}<a href="{% url "social:begin" "google-oauth2" %}"><button class="btn btn-danger" style="width: 40%">G+ Login</button></a>
<a href="{% url "social:begin" "facebook" %}"><button class="btn btn-primary" style="width: 40%">FB Login</button></a>{% endraw %}

이와 같이 button을 등록해 호출할 수 있습니다.

위 버튼을 누를 경우 각각 Google/Facebook의 Social Login페이지로 넘어갑니다.

수고하셨습니다!

widgets:

위 코드만으로도 약간의 조작을 통해 더 멋진 Social Login기능을 구현하실 수 있으리라 생각합니다.

Happy Coding!

Django에 Custom인증 붙이기

들어가기 전

widgets:

Django는 기본적으로 authentication을 내장하고 있고, User Model을 장고 자체가 가지고 있다.

UserModel의 경우 settings.py에서 AUTH_USER_MODEL을 커스텀 유저 모델로 지정해주면 프로젝트 전역에서 사용 가능하지만, 이번 글에서는 이 부분이 아니라 AUTH 처리를 추가할 수 있는지에 대해 알아볼 것이다.

프로젝트 폴더 구조는 아래와 같다. (django-admin startproject sample_project로 생성한 것과 같다. my_user라는 폴더를 만들고 안에 custom_auth.pymy_auth.py파일을 만든다.)

1
2
3
4
5
6
7
8
9
10
.
├── manage.py
├── my_user # 유저 모델을 다룰 곳
│   ├── custom_auth.py
│   └── my_auth.py
└── sample_project # 프로젝트 디렉토리
├── __init__.py
├── settings.py # 장고 프로젝트 settings
├── urls.py
└── wsgi.py

사용하는 경우

widgets:

예를들어, “OO커뮤니티 소속이라면, 우리 서비스에서도 커뮤니티 id와 pw로 로그인이 가능하게 하자”가 대표적인 예시가 될 수 있다.

위 문장을 좀더 풀어쓴다면 “OO커뮤니티에 로그인이 가능한 ID”를 받아 “OO커뮤니티의 인증”으로 “우리 서비스에도 로그인” 할 수 있게 하는 것이다.

만들어봅시다

widgets:

1. check_if_user 함수 만들기

우선 “OO커뮤니티 사이트에 로그인이 가능한 유저인지”를 확인해야 한다.
예를들어 “community-dummy”라는 사이트에 로그인하는 url이 /login이고, 유저만 볼 수 있는 페이지가 /login_requited_page라고 가정하자.
이 사이트에서는 /login_requited_page에 접속시 로그인된 상태라면 HTTP 200코드를, 로그인 되어있지 않다면 HTTP 401등의 에러코드를 전송한다고 가정하자.

그렇다면 우리는 파이썬의 requests모듈을 이용해 /login에 로그인 정보를 POST방식으로 전송하고 /login_requited_pageGET방식으로 접근해 HTTP코드를 .status_code를 통해 확인해보면 된다.
아래 코드를 확인해보자.

참고: requests는 pip install requests로 설치 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# custom_auth.py
import requests

def check_if_user(user_id, user_pw):
payload = {
'user_id': str(user_id),
'user_pw': str(user_pw)
}
with requests.Session() as s:
s.post('https://community-dummy.com/login', data=payload)
auth = s.get('https://community-dummy.com/login_requited_page')
if auth.status_code == 200: # 성공적으로 가져올 때
return True
else: # 로그인이 실패시
return False

우리는 이제 이 코드를 통해 유저가 우리 사이트에 입력한 id와 pw가 정확한(OO커뮤니티에 로그인 가능한)것인지를 확인할 수 있다.

2. 커스텀 UserBackend 만들기

우선 django 프로젝트가 사용하는 User모델을 가져오자.

1
2
3
4
# my_auth.py
from django.contrib.auth import get_user_model

UserModel = get_user_model()

위 방식으로 사용할 경우 Django의 기본 UserModel인 django.contrib.auth.models.User 뿐 아니라 settings.py에 따로 지정한 AUTH_USER_MODEL 클래스를 가져오게 된다.

참고: get_user_modelAUTH_USER_MODEL은 다르다.
django.contrib.authget_user_model은 유저모델 class를 반환하는 반면,
django.conf.settingsAUTH_USER_MODEL은 유저모델 지정을 str로 반환한다.

그리고 위에서 만든 custom_auth.py에서 check_if_user를 import해주자.

1
2
3
4
5
# my_auth.py
from django.contrib.auth import get_user_model
from .custom_auth import check_if_user # custom Auth성공시 True 아니면 False

UserModel = get_user_model() # django.contrib.auth.models.User대신 사용

이제 장고가 AUTHENTICATION_BACKENDS로서 추가적으로 사용할 UserBackend class를 만들어보자.

UserBackend클래스는 최소한 authenticate, user_can_authenticate, get_user라는 함수는 있어야 동작한다.

authenticate함수는 self, username, password를 인자로 받은 후, 정상적으로 인증된 경우 user 객체를 ‘하나’ 반환해야 하고, 없는 경우 None값을 반환해야 한다.

user_can_authenticate함수는 user객체를 인자로 받아서 is_active값을 가져와 활성화된 유저인지를 체크한다. (유저가 없거나 활성화된 경우 True, 비활성화된 경우 False)

get_user함수는 user_id를 인자로 받아 User객체를 pk로 참조해 user객체를 반환한다. 없는경우 None을 반환한다.

위 함수들을 작성하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# my_auth.py
from django.contrib.auth import get_user_model
from .custom_auth import check_if_user # custom Auth성공시 True 아니면 False

UserModel = get_user_model()

class UserBackend(object):
def authenticate(self, username=None, password=None):
if check_if_user(username, password): # OO커뮤니티 사이트 인증에 성공한 경우
try: # 유저가 있는 경우
user = UserModel.objects.get(username=username)
except UserModel.DoesNotExist: # 유저 정보가 없지만 인증 통과시 user 생성
user = UserModel(username=username)
user.is_staff = False
user.is_superuser = False
user.save()
# 여기서는 user.password를 저장하는 의미가 없음.(장고가 관리 못함)
return user
else: # OO 커뮤니티 사이트 인증에 실패한 경우, Django기본 User로 감안해 password검증
try:
user = UserModel.objects.get(username=username)
if user.check_password(password) and self.user_can_authenticate(user):
return user
except:
return None

def user_can_authenticate(self, user):
is_active = getattr(user, 'is_active', None) # 유저가 활성화 되었는지
return is_active or is_active is None # 유저가 없는 경우 is_active는 None이므로 True

def get_user(self, user_id):
try:
return UserModel.objects.get(pk=user_id) # 유저를 pk로 가져온다
except UserModel.DoesNotExist:
return None

3. settings.py에 AUTHENTICATION_BACKENDS 추가하기

장고에서 기본적으로 관리해주는 AUTHENTICATION_BACKENDS에는 django.contrib.auth.backends.ModelBackend가 있다. 하지만 위에서 우리가 만든 UserBackend를 추가해줘야 한다.

AUTHENTICATION_BACKENDS는 기본적으로 list로 구성되어있으며, 적혀진 순서대로 위에서부터 Auth을 진행한다.(실패시 다음 auth backend를 이용)

아래 코드와 같이 settings.py 파일 아래에 추가해 주자.

1
2
3
4
5
# settings.py
AUTHENTICATION_BACKENDS = [
'my_user.my_auth.UserBackend', # 우리가 만든 AUTH를 먼저 검사
'django.contrib.auth.backends.ModelBackend', # Django가 관리하는 AUTH
]

이렇게 추가해 줌으로서 django는 우리의 UserBackend를 이용해 유저를 관리하게 된다.

마무리 코드

widgets:

custom_auth 파일(진짜 OO커뮤니티 유저인가?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# custom_auth.py
import requests

def check_if_user(user_id, user_pw):
payload = {
'user_id': str(user_id),
'user_pw': str(user_pw)
}
with requests.Session() as s:
s.post('https://community-dummy.com/login', data=payload)
auth = s.get('https://community-dummy.com/login_requited_page')
if auth.status_code == 200: # 성공적으로 가져올 때
return True
else: # 로그인이 실패시
return False

my_auth 파일 (우리가 만든 UserBackend)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# my_auth.py
from django.contrib.auth import get_user_model
from .custom_auth import check_if_user # custom Auth성공시 True 아니면 False

UserModel = get_user_model()

class UserBackend(object):
def authenticate(self, username=None, password=None):
if check_if_user(username, password): # OO커뮤니티 사이트 인증에 성공한 경우
try: # 유저가 있는 경우
user = UserModel.objects.get(username=username)
except UserModel.DoesNotExist: # 유저 정보가 없지만 인증 통과시 user 생성
user = UserModel(username=username)
user.is_staff = False
user.is_superuser = False
user.save()
# 여기서는 user.password를 저장하는 의미가 없음.(장고가 관리 못함)
return user
else: # OO 커뮤니티 사이트 인증에 실패한 경우, Django기본 User로 감안해 password검증
try:
user = UserModel.objects.get(username=username)
if user.check_password(password) and self.user_can_authenticate(user):
return user
except:
return None

def user_can_authenticate(self, user):
is_active = getattr(user, 'is_active', None) # 유저가 활성화 되었는지
return is_active or is_active is None # 유저가 없는 경우 is_active는 None이므로 True

def get_user(self, user_id):
try:
return UserModel.objects.get(pk=user_id) # 유저를 pk로 가져온다
except UserModel.DoesNotExist:
return None

장고의 프로젝트 settings.py파일

1
2
3
4
5
# settings.py
AUTHENTICATION_BACKENDS = [
'my_user.my_auth.UserBackend', # 우리가 만든 AUTH를 먼저 검사
'django.contrib.auth.backends.ModelBackend', # Django가 관리하는 AUTH
]

나만의 웹 크롤러 만들기(2): Login with Session

좀 더 보기 편한 깃북 버전의 나만의 웹 크롤러 만들기가 나왔습니다!

이전게시글: 나만의 웹 크롤러 만들기 with Requests/BeautifulSoup

@2017.07.12 Update: 뉴클리앙으로 업데이트 됨에 따라 코드와 스크린샷이 업데이트 되었습니다.

웹 사이트를 로그인 하는데 있어 쿠키와 세션을 빼놓고 이야기하는 것은 불가능합니다.

이번 포스팅에서는 requests모듈을 이용해 로그인이 필요한 웹 사이트를 크롤링 하는 예제를 다룹니다.

웹은 대다수가 HTTP기반으로 동작합니다. 하지만 HTTP가 구현된 방식에서 웹 서버와 클라이언트는 지속적으로 연결을 유지한 상태가 아니라 요청(request)-응답(response)의 반복일 뿐이기 때문에, 이전 요청과 새로운 요청이 같은 사용자(같은 브라우저)에서 이루어졌는지를 확인하는 방법이 필요합니다. 이 때 등장하는 것이 ‘쿠키’와 ‘세션’입니다.

쿠키는 유저가 웹 사이트를 방문할 때 사용자의 브라우저에 심겨지는 작은 파일인데, Key - Value 형식으로 로컬 브라우저에 저장됩니다. 서버는 이 쿠키의 정보를 읽어 HTTP 요청에 대해 브라우저를 식별합니다.

그러나, 쿠키는 로컬에 저장된다는 근원적인 문제로 인해 악의적 사용자가 쿠키를 변조하거나 탈취해 정상적이지 않은 쿠키로 서버에 요청을 보낼 수 있습니다. 만약 ‘로그인 하였음’이라는 식별을, 로컬 쿠키만을 신뢰해 로그인을 한 상태로 서버가 인식한다면 쿠키 변조를 통해 마치 관리자나 다른 유저처럼 행동할 수도 있는 것이죠.(굉장히 위험합니다.)

이로 인해 서버측에서 클라이언트를 식별하는 ‘세션’을 주로 이용하게 됩니다.

세션은 브라우저가 웹 서버에 요청을 한 경우 서버 내에 해당 세션 정보를 파일이나 DB에 저장하고 클라이언트의 브라우저에 session-id라는 임의의 긴 문자열을 줍니다. 이때 사용되는 쿠키는 클라이언트와 서버간 연결이 끊어진 경우 삭제되는 메모리 쿠키를 이용합니다.

Requests의 Session

이전 게시글에서 다룬 requests모듈에는 Session이라는 도구가 있습니다.

1
2
3
4
import requests

# Session 생성
s = requests.Session()

Session은 위와 같은 방식으로 만들 수 있습니다.

이렇게 만들어진 세션은 이전 게시글에서의 requests위치를 대신하는데, 이전 게시글의 코드를 바꿔본다면 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# parser.py
import requests

# Session 생성
s = requests.Session()

# HTTP GET Request: requests대신 s 객체를 사용한다.
req = s.get('https://www.clien.net/service/')

# HTML 소스 가져오기
html = req.text
# HTTP Header 가져오기
header = req.headers
# HTTP Status 가져오기 (200: 정상)
status = req.status_code
# HTTP가 정상적으로 되었는지 (True/False)
is_ok = req.ok

코드를 with구문을 사용해 좀 더 정리하면 아래와 같습니다. 위 코드와 아래코드는 정확히 동일하게 동작하지만, 위쪽 코드의 경우 Session이 가끔 풀리는 경우가 있어 (5번중 한번 꼴) 아래 코드로 진행하는 것을 추천합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# parser.py
import requests

# Session 생성, with 구문 안에서 유지
with requests.Session() as s:
# HTTP GET Request: requests대신 s 객체를 사용한다.
req = s.get('https://www.clien.net/service/')
# HTML 소스 가져오기
html = req.text
# HTTP Header 가져오기
header = req.headers
# HTTP Status 가져오기 (200: 정상)
status = req.status_code
# HTTP가 정상적으로 되었는지 (True/False)
is_ok = req.ok

로그인하기

로그인을 구현하기 위한 예시로 클리앙에 로그인 해 클리앙 장터를 크롤링 해 봅시다.

크롬 개발자 도구 중 Inspect(검사)를 이용해 로그인 폼 필드의 name값들을 알아봅시다.(폼 위에서 마우스 오른쪽 버튼을 클릭하고 검사를 눌러주세요.)

아래 스크린샷 우측을 확인해 봅시다. form 태그 안에 input필드가 여러개가 있는 것을 알 수 있습니다.

조금 더 상세하게 뜯어봅시다. 아래 스크린샷을 보시면 input필드들의 name_csrf,userID,userPassword,remember-me가 있는 것을 볼 수 있습니다. 또한, 로그인 버튼을 누르면 auth.login()라는 자바스크립트 함수가 먼저 실행되는 것을 볼 수 있습니다.

로그인을 구현하기 전, HTML form에 대해 간단하게 알아봅시다.

HTML form Field에서는 name:입력값이라는 Key:Value식으로 데이터를 전달합니다.(주로 POST방식)

클리앙 로그인 폼 필드의 경우 userID:사용자id, userPassword:사용자pw라는 세트로 입력을 받는 것을 볼 수 있습니다.

그리고 약간 특이해 보이는 _csrf이라는 것도 있습니다. 원래 CSRF는 사용자의 요청이 악의적이거나 제 3자에 의해 변조된(해킹된) 요청이 아닌지 확인해주는 보안 도구중 하나입니다. 세션과 연결되어 폼을 전달할때 폼의 안정성을 높여줍니다. 새로고침하시면 매번 달라지는 CSRF값을 보실 수 있습니다. 그리고 CSRF를 사용하는 경우 CSRF값이 없는 폼 전송은 위험한 요청으로 생각하고 폼을 받아들이지 않습니다.(즉, 로그인이 되지 않습니다.) 따라서 우리는 _csrf라는 것도 함께 전송해 줘야 합니다. 따라서 메인 화면을 먼저 가져와 _csrf필드를 가져오고 로그인을 해야 합니다.

이전 클리앙은 CSRF검증이 없었습니다. 이번 업데이트를 하면서 클리앙의 보안이 전반적으로 올라갔습니다. 좋은 변화입니다!

다음으로는 auth.login()이라는 함수를 살펴봅시다. 함수를 살펴보면 그냥 입력 유무만 확인하는 심플한 함수입니다. 사실 이것보다 더 길지만, 실제로 login함수에서 사용되는 코드 부분은 이부분이 전부이기 때문에 뒷부분을 잘랐습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Auth() {
var _this = this; // _this에 Auth라는 함수를 넣었습니다.
_this.env = {};
_this.env.form = $('#loginForm'); // 로그인 폼입니다. id, pw, _csrf 등을 받는다고 위에서 확인했죠?
_this.env.iptUserId = _this.env.form.find('*[name=userId]'); // 사용자가 폼에 입력한 ID입니다.
_this.env.iptUserPassWord = _this.env.form.find('*[name=userPassword]'); // 사용자가 폼에 입력한 PW입니다.

_this.loginValidate = function() {
var isValid = true; // 아무 문제가 없다면(id나 pw가 빈칸이 아니라면) true를 반환하는 함수입니다.
if (_this.env.iptUserId.val().trim() == '') { // 아이디가 빈칸이면 false죠?
alert('아이디를 입력하세요.');
_this.env.iptUserId.focus();
isValid = false;
return isValid;
}
if (_this.env.iptUserPassWord.val().trim() == '') { // 비번이 빈칸이어도 false가 됩니다.
alert('비밀번호를 입력하세요.');
_this.env.iptUserPassWord.focus();
isValid = false;
return isValid;
}
return isValid;
};

_this.login = function() {
var isValid = _this.loginValidate(); // 방금 본 아이디/비번이 빈칸인지 확인하기
if (isValid) { // 빈칸이 아니라면 ->
_this.env.form.attr({ // 폼 속성을 정의해 줍시다.
method: 'POST', // 폼 전송 방식은 'POST'이고,
action: BASE_URL + '/login' // 폼 전송하는 주소는 https://www.clien.net/service/login 이네요!
});
_this.env.form.submit(); // 진짜로 폼을 전송해줍니다.
}
};
}

위 자바스크립트 코드에서 알게된 것은 아이디와 비밀번호 폼에 빈칸이 없다면 POST방식으로 https://www.clien.net/service/login에 폼을 전송해 로그인을 한다는 것입니다.

한번 이 주소에 폼 값들만 넣어서 전송해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# parser.py
import requests

# 로그인할 유저정보를 넣어주자 (모두 문자열)
LOGIN_INFO = {
'userId': '사용자이름',
'userPassword': '사용자패스워드'
}

# Session 생성, with 구문 안에서 유지
with requests.Session() as s:
# HTTP POST request: 로그인을 위해 POST url와 함께 전송될 data를 넣어주자.
login_req = s.post('https://www.clien.net/service/login', data=LOGIN_INFO)
# 어떤 결과가 나올까요?
print(login_req.status_code)

이런! 404가 나와버렸네요. 제대로 로그인이 되지 않은 것 같아요. 아마 _csrf값이 없어서가 아닐까요?

1
2
> python parsing.py
404

그렇다면 코드를 조금 더 수정해 봅시다. 우선 클리앙 공식 홈페이지에 들어가 form에 들어있는 _csrf값을 가져와 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# parser.py
import requests
from bs4 import BeautifulSoup as bs

# 로그인할 유저정보를 넣어줍시다. (모두 문자열입니다!)
LOGIN_INFO = {
'userId': 'myidid',
'userPassword': 'mypassword123'
}

# Session 생성, with 구문 안에서 유지
with requests.Session() as s:
# 우선 클리앙 홈페이지에 들어가 봅시다.
first_page = s.get('https://www.clien.net/service')
html = first_page.text
soup = bs(html, 'html.parser')
csrf = soup.find('input', {'name': '_csrf'}) # input태그 중에서 name이 _csrf인 것을 찾습니다.
print(csrf['value']) # 위에서 찾은 태그의 value를 가져옵니다.

# 이제 LOGIN_INFO에 csrf값을 넣어줍시다.
# (p.s.)Python3에서 두 dict를 합치는 방법은 {**dict1, **dict2} 으로 dict들을 unpacking하는 것입니다.
LOGIN_INFO = {**LOGIN_INFO, **{'_csrf': csrf['value']}}
print(LOGIN_INFO)

# 이제 다시 로그인을 해봅시다.
login_req = s.post('https://www.clien.net/service/login', data=LOGIN_INFO)
# 어떤 결과가 나올까요? (200이면 성공!)
print(login_req.status_code)

와우! 200이 나온걸 보니 성공적으로 로그인이 된 것 같아요.

진짜 데이터를 가져와봅시다

이제 우리 코드를 좀 더 멋지게 만들어 봅시다. 로그인이 실패한 경우 Exception을 만들고, 성공일 경우에는 회원 장터의 게시글을 가져와봅시다.

위 스크린샷처럼 오른쪽 버튼을 누르고 Copy > Copy selector를 눌러주면 #div_content > div.post-title > div.title-subject > div라는 CSS Selector가 나옵니다. 이 HTML문서에서 이 제목만을 콕 하고 찾아줍니다.

본문도 같은 방식으로 찾아줍시다. 다만 p태그가 아니라 글 전체를 담고있는 #div_content > div.post.box > div.post-content > div.post-article.fr-view을 가져와봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# parser.py
import requests
from bs4 import BeautifulSoup as bs

# 로그인할 유저정보를 넣어줍시다. (모두 문자열입니다!)
LOGIN_INFO = {
'userId': 'myidid',
'userPassword': 'mypassword123'
}

# Session 생성, with 구문 안에서 유지
with requests.Session() as s:
# 우선 클리앙 홈페이지에 들어가 봅시다.
first_page = s.get('https://www.clien.net/service')
html = first_page.text
soup = bs(html, 'html.parser')
csrf = soup.find('input', {'name': '_csrf'}) # input태그 중에서 name이 _csrf인 것을 찾습니다.
print(csrf['value']) # 위에서 찾은 태그의 value를 가져옵니다.

# 이제 LOGIN_INFO에 csrf값을 넣어줍시다.
# (p.s.)Python3에서 두 dict를 합치는 방법은 {**dict1, **dict2} 으로 dict들을 unpacking하는 것입니다.
LOGIN_INFO = {**LOGIN_INFO, **{'_csrf': csrf['value']}}
print(LOGIN_INFO)

# 이제 다시 로그인을 해봅시다.
login_req = s.post('https://www.clien.net/service/login', data=LOGIN_INFO)
# 어떤 결과가 나올까요? (200이면 성공!)
print(login_req.status_code)
# 로그인이 되지 않으면 경고를 띄워줍시다.
if login_req.status_code != 200:
raise Exception('로그인이 되지 않았어요! 아이디와 비밀번호를 다시한번 확인해 주세요.')

# -- 여기서부터는 로그인이 된 세션이 유지됩니다 --
# 이제 장터의 게시글 하나를 가져와 봅시다. 아래 예제 링크는 중고장터 공지글입니다.
post_one = s.get('https://www.clien.net/service/board/rule/10707408')
soup = bs(post_one.text, 'html.parser') # Soup으로 만들어 줍시다.
# 아래 CSS Selector는 공지글 제목을 콕 하고 집어줍니다.
title = soup.select('#div_content > div.post-title > div.title-subject > div')
contents = soup.select('#div_content > div.post.box > div.post-content > div.post-article.fr-view')
# HTML을 제대로 파싱한 뒤에는 .text속성을 이용합니다.
print(title[0].text) # 글제목의 문자만을 가져와봅시다.
# [0]을 하는 이유는 select로 하나만 가져와도 title자체는 리스트이기 때문입니다.
# 즉, 제목 글자는 title이라는 리스트의 0번(첫번째)에 들어가 있습니다.
print(contents[0].text) # 글내용도 마찬가지겠지요?

코드를 실행해 봅시다.

잘 가져옵니다 :)

그러나, 위 코드가 안먹힌다면?

일부 사이트의 경우 프론트 브라우저 단에서 ID와 PW를 이용해 암호화된 전송값을 보내는 경우가 있습니다.(대표적으로 네이버가 이렇습니다.) 또한, SPA등으로 인해 PageSource을 가져오는 것이 불충분한 경우가 자주 있습니다.

물론 오늘처럼 JS파일을 분석해 수동으로 data에 넣어주는 방법도 있지만, 브라우저를 직접 다뤄서 사람이 로그인하듯 크롤링을 해보면 어떨까요?

다음 포스팅에서 좀 더 간편히 실제 브라우저(혹은 Headless브라우저)를 이용해 로그인부터 크롤링까지, 간편하게 해보는 방법을 알아봅시다.

다음 가이드: 나만의 웹 크롤러 만들기(3): Selenium으로 무적 크롤러 만들기

업데이트 후기

2017년 7월 12일, 올해 초(1월 20일)에 작성한 인기 크롤링글 대상인 클리앙이 바뀌어 업데이트가 필요했습니다. 사실 예전 코드를 업데이트 하는 것도 사실상 새 글을 쓰는 것과 같은 시간과 노력이 듭니다. 하지만 오래된 정보를 두는 것보다 새로운 정보를 두는 것이 낫다고 생각해 업데이트를 했으나..!

클리앙을 이용하는데 덧글 쓰기/글 쓰기 빼고 글을 읽는 것에 제한은 회원장터조차도 제한이 없어졌더군요. (눈물)

그래도 이 가이드를 기반으로 다른 사이트 로그인 하는데 조금 더 쉬워지기를 바랍니다.

나만의 웹 크롤러 만들기 with Requests/BeautifulSoup

좀 더 보기 편한 깃북 버전의 나만의 웹 크롤러 만들기가 나왔습니다!

(@2017.03.18) 본 블로그 테마가 업데이트되면서 구 블로그의 URL은 https://beomi.github.io/beomi.github.io_old/로 변경되었습니다. 예제 코드에서는 변경을 완료하였지만 캡쳐 화면은 변경하지 않았으니 유의 바랍니다.

웹 크롤러란?

우리가 어떤 정보를 브라우저에서만 보는 것 뿐 아니라 그 정보들을 내가 이용하기 편한 방식(ex: json)으로 로컬에 저장하고 싶을 때가 있다.

HTTrack의 경우에는 웹을 그대로 자신의 컴퓨터로 복사를 해오지만, 내가 원하는 방식으로의 가공까지는 제공해주지 않는다.

Python을 이용하면 간단한 코드 몇줄 만으로도 쉽게 웹 사이트에서 원하는 정보만을 가져올 수 있다.

웹에서 정보 가져오기

Requests

Python에는 requests라는 유명한 http request 라이브러리가 있다.

설치하기

1
pip install requests

pip로 간단하게 설치가 가능하다.

이용방법

Python 파일 하나(ex: parser.py)를 만들어 requests를 import 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# parser.py
import requests

# HTTP GET Request
req = requests.get('https://beomi.github.io/beomi.github.io_old/')

# HTML 소스 가져오기
html = req.text
# HTTP Header 가져오기
header = req.headers
# HTTP Status 가져오기 (200: 정상)
status = req.status_code
# HTTP가 정상적으로 되었는지 (True/False)
is_ok = req.ok

위 코드에서 우리가 사용할 것은 HTML 소스를 이용하는 것이다. 따라서 html=req.text를 이용한다.

BeautifulSoup

Requests는 정말 좋은 라이브러리이지만, html을 ‘의미있는’, 즉 Python이 이해하는 객체 구조로 만들어주지는 못한다. 위에서 req.text는 python의 문자열(str)객체를 반환할 뿐이기 때문에 정보를 추출하기가 어렵다.

따라서 BeautifulSoup을 이용하게 된다. 이 BeautifulSoup은 html 코드를 Python이 이해하는 객체 구조로 변환하는 Parsing을 맡고 있고, 이 라이브러리를 이용해 우리는 제대로 된 ‘의미있는’ 정보를 추출해 낼 수 있다.

설치하기

1
pip install bs4

BeautifulSoup을 직접 쳐서 설치하는 것도 가능하지만, bs4라는 wrapper라이브러리를 통해 설치하는 방법이 더 쉽고 안전하다.

이용방법

위에서 이용한 parser.py파일을 좀 더 다듬어 보자.

1
2
3
4
5
6
7
8
9
10
11
12
# parser.py
import requests
from bs4 import BeautifulSoup

# HTTP GET Request
req = requests.get('https://beomi.github.io/beomi.github.io_old/')
# HTML 소스 가져오기
html = req.text
# BeautifulSoup으로 html소스를 python객체로 변환하기
# 첫 인자는 html소스코드, 두 번째 인자는 어떤 parser를 이용할지 명시.
# 이 글에서는 Python 내장 html.parser를 이용했다.
soup = BeautifulSoup(html, 'html.parser')

이제 soup 객체에서 원하는 정보를 찾아낼 수 있다.

BeautifulSoup에서는 여러가지 기능을 제공하는데, 여기서는 select를 이용한다. select는 CSS Selector를 이용해 조건과 일치하는 모든 객체들을 List로 반환해준다.

예시로 이 블로그의 모든 제목을 가져와 보도록 하자.

크롬에 내장된 검사도구(요소 위에서 우측 클릭 후 검사)를 이용해보면 현재 title은 a 태그로 구성되어있다는 것을 알 수 있다. 이 상황에서 모든 a 태그를 가져올 수도 있지만, 보다 정확하게 가져오기 위해 CSS Selector를 확인해 보자.

확인해보니 아래와 같은 코드가 나왔다.

1
body > h3:nth-child(4) > a

하지만 :nth-child(4) 등이 붙어있는 것으로 보아 현재 요소를 ‘정확하게’ 특정하고 있기 때문에, 좀 더 유연하게 만들어 주기 위해 아래와 같이 selector를 바꿔준다.(위 코드는 단 하나의 링크만을 특정하고, 아래 코드는 css selector에 일치하는 모든 요소를 가리킨다.)

1
h3 > a

이제 parsing.py파일을 더 다듬어 보자.

1
2
3
4
5
6
7
8
9
10
11
# parser.py
import requests
from bs4 import BeautifulSoup

req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
# CSS Selector를 통해 html요소들을 찾아낸다.
my_titles = soup.select(
'h3 > a'
)

위 코드에서 my_titles는 string의 list가 아니라 soup객체들의 list이다. 따라서 태그의 속성들도 이용할 수 있는데, a 태그의 경우 href속성이 대표적인 예시다.

soup객체는 <태그></태그>로 구성된 요소를 Python이 이해하는 상태로 바꾼 것이라 볼 수 있다. 따라서 여러가지로 조작이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# parser.py
import requests
from bs4 import BeautifulSoup

req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
my_titles = soup.select(
'h3 > a'
)
# my_titles는 list 객체
for title in my_titles:
# Tag안의 텍스트
print(title.text)
# Tag의 속성을 가져오기(ex: href속성)
print(title.get('href'))

위와 같이 코드를 처리할 경우 a 태그 안의 텍스트와 a 태그의 href속성의 값을 가져오게 된다. 위 코드에서 title 객체는 python의 dictionary와 같이 태그의 속성들을 저장한다. 따라서 title.get('속성이름')title['속성이름']처럼 이용할 수 있다.

select를 통해 요소들을 가져온 이후에는 각자가 생각하는 방식으로 python코드를 이용해 저장하면 된다.

정리 예제

아래 코드는 크롤링한 데이터를 Python파일와 같은 위치에 result.json을 만들어 저장하는 예제다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# parser.py
import requests
from bs4 import BeautifulSoup
import json
import os

# python파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
my_titles = soup.select(
'h3 > a'
)

data = {}

for title in my_titles:
data[title.text] = title.get('href')

with open(os.path.join(BASE_DIR, 'result.json'), 'w+') as json_file:
json.dump(data, json_file)

다음 가이드

다음 가이드에서는 Session을 이용해 어떻게 웹 사이트에 로그인을 하고, 로그인 상태를 유지하며 브라우징을 하는지에 대해 알아보겠습니다.

다음 가이드: 나만의 웹 크롤러 만들기(2): Login With Session

Celery로 TelegramBot 알림 보내기

Celery는 비동기 큐이지만 주기적 Task도 잘한다

Celery는 async/비동기적으로 특정한 작업을 돌리기 위해 자주 사용한다. 특히, django와는 찰떡궁합이라고 알려져있다.
하지만 이 celery는 설정이 어렵다면 어렵고, 쉽다면 쉬운편이다.

먼저 Celery를 “쓰려면” 어떤 것들이 필요한지 체크해보자.

Celery 준비물

  • pip를 통해 설치된 Celery가 필요하다.
1
$ pip install celery
  • RabbitMQ나 Redis등의 큐 중간 저장소가 필요하다. RabbitMQ를 설치해보자.
1
$ brew install rabbitmq
  • 이후 .bashrc.zshrc의 마지막 줄에 아래 코드를 추가해준다.
1
export PATH=$PATH:/usr/local/sbin

이로서 rabbitmq-server라는 명령어로 rabbitmq를 실행할 수 있다.

셀러리가 돌아갈 파이썬 파일 만들기

아래와 같이 코드를 작성해 보자. (celery_parser.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# celery_parser.py
from celery import Celery

# Celery Setup
app = Celery()
app.conf.timezone = 'Asia/Seoul'

@app.on_after_configure.connect
def periodic_parser(sender, **kwargs):
sender.add_periodic_task(5.0, hello(), name='hello?')

@app.task
def hello():
print('hello!')

위 코드를 작성한 후, 쉘을 두 창을 켠 후 각각 아래 코드를 입력해 준다. (celery_parser.py와 같은 폴더에서)

1
2
3
4
celery worker -A celery_parser --loglevel=info

<이용법>
celery worker -A 파이썬파일이름 --loglevel=info

위 코드는 Celery의 get_url함수, 즉 app의 Task함수가 실제로 구동될 worker이며,
아래 코드는 periodic_parser함수 안에서 정의된 sender.add_periodic_task에 의해 첫번째 인자로 전달된 5.0초, 두번째 인자로 전달된 hello 함수를 실행하게 하는 Celery의 beat이다.

1
2
3
4
celery beat -A celery_parser  --loglevel=info

<이용법>
celery beat -A 파이썬파일이름 --loglevel=info

TelegramBot 설정하기

python에서 telegram bot 사용 가이드

텔레그램 봇을 Python에서 이용하는 좋은 가이드가 있다.

python에서 telegram bot 사용하기

위 링크를 참고해서 pip로 python-telegram-bot을 설치하고, 새 봇을 만든 후 token과 id값을 받아오자.

requests를 이용해 site의 변화 유무 체크하기

우선 파이썬 파일을 수정하기 전에 target.json라는 환경변수용 json파일을 아래와 같이 만들어 주자.

1
2
3
4
5
{
"BOT_TOKEN":"위에서 받은 숫자9자리:영문+숫자+특수문자 긴것",
"URL":"변화유무를 체크할 URL",
"CHAT_ID":"위에서 받은 id"
}

이제 celery_parser.py에서 target.json파일을 불러온 후, 변수로 등록해주고, telegram bot 객체를 만들어 준 후 sendMessage를 이용해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# celery_parser.py
from celery import Celery

import requests
import json
import os
import datetime

import telegram

# Celery Setup
app = Celery()
app.conf.timezone = 'Asia/Seoul'

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# Parsing/Telegram Environ loads
with open(os.path.join(BASE_DIR, "target.json")) as f:
env = json.loads(f.read())
BOT_TOKEN = env['BOT_TOKEN']
URL = env['URL']
CHAT_ID = env['CHAT_ID']

bot = telegram.Bot(token=BOT_TOKEN)

bot.sendMessage(chat_id=CHAT_ID, text='Started!')

@app.on_after_configure.connect
def periodic_parser(sender, **kwargs):
sender.add_periodic_task(5.0, get_url.s(URL), name='send working time')

@app.task
def get_url(url):
req = requests.get(url)
f = open('temp/req.txt', 'w+')
previous_html = f.read()
new_html = req.text
bot.sendMessage(chat_id=CHAT_ID, text='Working/{}'.format(datetime.datetime.now()))

if previous_html == new_html:
bot.sendMessage(chat_id=CHAT_ID, text='working...')
else:
#TODO: BOT NOTICE
bot.sendMessage(chat_id=CHAT_ID, text='{} 에 변경이 있습니다.'.format(url))

위 코드는 URL에 접속해 html로 저장 후 5초 후 다음 접속 시 사이트에 변동사항이 있으면 변동이 있다는 Telegram 알림을 보내준다.

Virtualenv/VirtualenvWrapper OS별 설치&이용법

Virtualenv란?

Virtualenv란 시스템 OS에 설치된 주 python뿐만 아니라 여러 버전의 Python과 프로젝트별로 다른 종류의 라이브러리를 사용하는 것에 있어 가장 핵심된 기능을 제공합니다.

예를들어, 어떤 옛날 프로젝트에서는 Python2.7버전에 pip로 Django1.6을 사용했다고 가정해봅시다. 하지만 이번에 새로 시작하는 프로젝트는 Python3.6에 pip로 Django1.10을 사용하려고 합니다. 물론 가장 쉬운 방법은 개발 환경별로 다른 컴퓨터를 사용하는 것이지만, 공간적/금전적/편의적으로 어렵습니다.

따라서 우리는 Python실행파일과 pip로 설치된 라이브러리들을 독립된 폴더에 넣어버리는 방법을 선택할 수 있는데, 이것이 Virtualenv의 핵심입니다.

아래 가이드는 OS별로 나누어져있습니다. [MAC OS가이드] [LINUX 가이드(UBUNTU)] [WINDOWS 가이드]

2016.12.30 기준 MAC OS가 완성되어있습니다.

MAC OS(OS X)의 경우

Virtualenv를 설치해보자 [MAC OS]

MAC OS에는 시스템 전역에 기본적으로 Python2가 설치되어있기 때문에 아래 명령어로 쉽게 pip를 설치할 수 있습니다.

1
$ sudo easy_install pip

만약 sudo로 시스템 전역에 설치하기가 싫다면 HomeBrew를 이용해 Python을 유저영역에 설치할 수도 있습니다.

1
2
$ brew install python
#Python3의 경우는 brew install python3)

pip가 성공적으로 설치되었는지 확인하려면 다음 명령어로 pip의 버전을 확인해 보면 됩니다.

1
2
$ pip -V
#Python3 pip의 경우에는 pip3 -V

만약 pip나 pip3이라는 명령어가 먹히지 않는다면 아래의 명령어로 Python의 모듈로서 pip를 호출할 수 있습니다.

1
2
3
4
5
# Python2의 경우
$ python -m pip -V

# Python3의 경우
$ python3 -m pip -V

Virtualenv와 VirtualenvWrapper는 pip를 통해 설치가 가능합니다.

1
2
3
4
5
# Python2의 경우
$ pip install virtualenv virtualenvwrapper

# Python3의 경우
$ pip3 install virtualenv virtualenvwrapper

만약 pip/pip3 명령이 먹지 않는다면 아래 명령어로 대체할 수 있습니다.
(시스템에 easy_install로 pip를 설치한 경우 sudo권한이 필요할 수 있는데, 이때는 sudo pip install으로 명령어 앞에 sudo를 붙여줍시다.)

1
2
3
4
5
# Python2 pip의 경우
$ python -m pip install virtualenv virtualenvwrapper

# Python3 pip의 경우
$ python3 -m pip install virtualenv virtualenvwrapper

지금까지 사용한 pippip3은 virtualenv를 어느 pip에 설치할까에 대한 내용일 뿐, 파이썬 가상환경에 어떤 Python이 설치될지와는 무관합니다.

Virtualenv의 기본적 명령어 [MAC OS]

Virtualenv는 기본적으로 아래의 명령어로 동작합니다.

1
2
3
4
$ virtualenv --python=파이썬버전 가상환경이름
# ex)
# $ virtualenv --python=python3.5 test_env
# $ virtualenv --python=python2.7 test_env2

이와 같이 Python버전을 명시해주고 가상환경을 만들 수 있습니다. (단, 선택할 Python은 시스템에 깔려있는 버전이어야 합니다.)

만약

와 같이 The path x.x does not exist라는 에러가 난다면 PYTHON의 PATH을 절대경로로 맞춰줘야 합니다. which python3을 했을 때 /usr/bin/python3이 나왔다면, virtualenv --python=/usr/bin/python3와 같이 절대경로로 입력해주시면 됩니다.

만든 가상환경에 진입(가상환경을 활성화)하려면 아래 명령어를 이용하면 됩니다.

1
$ source 가상환경이름/bin/activate

Python3이 설치된 test_env로 진입한 경우

Python2가 설치된 test_env2로 진입한 경우

각각 다른 python버전이 실행되고 있다는 것을 알 수 있습니다.

이후 pip를 통해 외부 모듈과 라이브러리들을 설치하는 경우, source 명령어로 가상환경에 진입하지 않으면 라이브러리들을 불러쓸 수 없게됩니다. 즉, 프로젝트 별로 다른 라이브러리만이 설치된 환경을 구성한 것이죠.

VirtualenvWrapper 설정하기

VirtualEnv를 사용하기 위해서는 source를 이용해 가상환경에 진입합니다. 그러나, 이 진입 방법은 가상환경이 설치된 위치로 이동해야되는 것 뿐 아니라 가상환경이 어느 폴더에 있는지 일일이 사용자가 기억해야 하는 단점이 있습니다. 이를 보완하기 위해 VirtualenvWrapper를 사용합니다.

또한, VirtualenvWrapper를 사용할 경우 터미널이 현재 위치한 경로와 관계없이 가상환경을 활성화할 수 있다는 장점이 있습니다.

VirtualenvWrapper는 .bashrc.zshrc에 약간의 설정과정을 거쳐야 합니다.

우선 홈 디렉토리로 이동해보세요.

1
$ cd ~

가상환경이 들어갈 폴더 .virtualenvs를 만들어주세요.

1
$ mkdir ~/.virtualenvs

그리고 홈 디렉토리의 .bashrc.zshrc의 파일 제일 마지막에 아래 코드를 복사해 붙여넣어줍시다.
(파일이 없다면 만들어 사용하시면 됩니다.)

1
2
3
4
# python virtualenv settings
export WORKON_HOME=~/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON="$(which python3)" # Usage of python3
source /usr/local/bin/virtualenvwrapper.sh

저장하고 나온 후 터미널을 종료후 새로 켜주면, VirtualenvWrapper의 명령어들을 사용할 수 있습니다.

만약 /usr/local/bin/virtualenvwrapper.sh파일이 존재하지 않는다면 다음 명령어로 virtualenvwrapper.sh파일을 찾아서 위 코드를 바꿔 사용하세요.

1
find /usr -name virtualenvwrapper.sh

VirtualenvWrapper 명령어들

VirtualenvWrapper의 명령어는 여러가지가 존재하지만, 이 포스팅에서는 기본적인 것만 다루고 넘어갑니다.

  • 가상환경 만들기
1
2
3
$ mkvirtualenv 가상환경이름
# 예시
# $ mkvirtualenv test_env3

mkvirtualenv 명령어를 사용할 경우 홈 디렉토리의 .virtualenvs폴더 안에 가상환경이름을 가진 폴더(test_env3)가 생깁니다.

  • 가상환경 지우기
1
2
3
$ rmvirtualenv 가상환경이름
# 예시
# $ rmvirtualenv test_env3

rmvirtualenv 명령어를 사용할 경우 mkvirtualenv로 만든 가상환경을 지워줍니다.

만든 가상환경을 지우는 방법은 이방법 뿐 아니라 홈 디렉토리의 .virtualenvs폴더 안의 가상환경이름을 가진 폴더를 지우는 방법도 있습니다.

  • 가상환경 진입하기 + 가상환경 목록 보기
1
2
3
4
5
6
$ workon 가상환경이름
# 가상환경으로 진입시 앞에 (가상환경이름)이 붙습니다.
(가상환경이름) $
# 예시
# $ workon test_env3
# (test_env3) $

workon명령어를 통해 mkvirtualenv로 만든 가상환경으로 진입할 수 있습니다.

workon명령어를 가상환경이름 없이 단순하게 칠 경우, 현재 만들어져있는 가상환경의 전체 목록을 불러옵니다.

1
2
$ workon
test_env3
  • 가상환경 빠져나오기
1
2
3
4
5
(가상환경이름) $ deactivate
$
# 예시
# (test_env3) $ deactivate
# $

가상환경에서 빠져나오는 것은 다른것들과 동일하게 deactivate명령어로 빠져나올 수 있습니다.

LINUX(UBUNTU)의 경우

Virtualenv를 설치해 보자 [LINUX(UBUNTU)]

Ubuntu의 경우에는 14버전 기준으로 Python2와 Python3이 기본적으로 설치되어있습니다.

Ubuntu16에서는 Python3이 기본입니다.

하지만 pip/pip3이 설치되어있지 않을 수 있기 때문에 python-pippython3-pip를 설치해야 합니다.

1
2
3
4
5
6
# APT를 업데이트
$ sudo apt-get update && apt-get upgrade -y
# Python2를 이용할 경우
$ sudo apt-get install python-pip python-dev
# Python3을 이용할 경우
$ sudo apt-get install python3-pip python3-dev

python-devpython3-dev를 설치하지 않아도 됩니다. 하지만 이후 정상적 동작을 보장할 수 없습니다.

pip 설치가 완료되었는지 확인하려면 아래 명령어를 입력해보면 됩니다.
(이번 게시글에서는 python3-pip로 진행합니다. Python2의 pip를 이용하시려면 python3-pip대신 python-pip를 설치하셔서 pip명령어를 사용하세요.)

1
2
3
4
# Python2 pip의 경우
$ pip -V
# Python3 pip의 경우
$ pip3 -V

이제 pip설치가 완료되었으므로 Virtualenv와 VirtualenvWrapper를 설치해보겠습니다.

1
2
3
4
5
# Python2의 경우
$ pip install virtualenv virtualenvwrapper

# Python3의 경우
$ pip3 install virtualenv virtualenvwrapper

만약 pip/pip3 명령이 먹지 않는다면 아래 명령어로 대체할 수 있습니다.
(시스템에 root권한으로 pip를 설치한 경우 sudo권한이 필요할 수 있는데, 이때는 sudo pip install으로 명령어 앞에 sudo를 붙여줍시다.)

1
2
3
4
5
# Python2 pip의 경우
$ python -m pip install virtualenv virtualenvwrapper

# Python3 pip의 경우
$ python3 -m pip install virtualenv virtualenvwrapper

지금까지 사용한 pippip3은 virtualenv를 어느 pip에 설치할까에 대한 내용일 뿐, 파이썬 가상환경에 어떤 Python이 설치될지와는 무관합니다.

Virtualenv의 기본적 명령어 [LINUX(UBUNTU)]

Virtualenv는 기본적으로 아래의 명령어로 동작합니다.

1
2
3
4
$ virtualenv --python=파이썬버전 가상환경이름
# ex)
# $ virtualenv --python=python3.5 py3_env
# $ virtualenv --python=python2.7 test_env2

만약 virtualenv 라는 명령이 먹히지 않는다면 python3 -m virtualenv(python2는 python -m virtualenv)명령어를 이용하거나, 쉘을 껐다가 다시 켜주세요.

만약

와 같이 The path x.x does not exist라는 에러가 난다면 PYTHON의 PATH을 절대경로로 맞춰줘야 합니다. which python3을 했을 때 /usr/bin/python3이 나왔다면, virtualenv --python=/usr/bin/python3와 같이 절대경로로 입력해주시면 됩니다.

이와 같이 Python버전을 명시해주고 가상환경을 만들 수 있습니다. (단, 선택할 Python은 시스템에 깔려있는 버전이어야 합니다. Ubuntu16의 경우 python2이 깔려있지 않을 수 있습니다.)

만든 가상환경에 진입(가상환경을 활성화)하려면 아래 명령어를 이용하면 됩니다.

1
$ source 가상환경이름/bin/activate

Python3이 설치된 py3_env로 진입한 경우

이후 pip를 통해 외부 모듈과 라이브러리들을 설치하는 경우, source 명령어로 가상환경에 진입하지 않으면 라이브러리들을 불러쓸 수 없게됩니다. 즉, 프로젝트 별로 다른 라이브러리만이 설치된 환경을 구성한 것이죠.

VirtualenvWrapper 설정하기 [LINUX(UBUNTU)]

VirtualEnv를 사용하기 위해서는 source를 이용해 가상환경에 진입합니다. 그러나, 이 진입 방법은 가상환경이 설치된 위치로 이동해야되는 것 뿐 아니라 가상환경이 어느 폴더에 있는지 일일이 사용자가 기억해야 하는 단점이 있습니다. 이를 보완하기 위해 VirtualenvWrapper를 사용합니다.

또한, VirtualenvWrapper를 사용할 경우 터미널이 현재 위치한 경로와 관계없이 가상환경을 활성화할 수 있다는 장점이 있습니다.

VirtualenvWrapper는 .bashrc.zshrc에 약간의 설정과정을 거쳐야 합니다.

우선 홈 디렉토리로 이동해보세요.

1
$ cd ~

가상환경이 들어갈 폴더 .virtualenvs를 만들어주세요.

1
$ mkdir ~/.virtualenvs

그리고 홈 디렉토리의 .bashrc.zshrc의 파일 제일 마지막에 아래 코드를 복사해 붙여넣어줍시다.
(파일이 없다면 만들어 사용하시면 됩니다.)

1
2
3
4
# python virtualenv settings
export WORKON_HOME=~/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON='$(command \which python3)' # Usage of python3
source /usr/local/bin/virtualenvwrapper.sh

저장하고 나온 후 터미널을 종료후 새로 켜주면, VirtualenvWrapper의 명령어들을 사용할 수 있습니다.

만약 /usr/local/bin/virtualenvwrapper.sh파일이 존재하지 않는다면 다음 명령어로 virtualenvwrapper.sh파일을 찾아서 위 코드를 바꿔 사용하세요.

1
find /usr -name virtualenvwrapper.sh

VirtualenvWrapper 명령어들 [LINUX(UBUNTU)]

VirtualenvWrapper의 명령어는 여러가지가 존재하지만, 이 포스팅에서는 기본적인 것만 다루고 넘어갑니다.

  • 가상환경 만들기
1
2
3
$ mkvirtualenv 가상환경이름
# 예시
# $ mkvirtualenv test_env3

mkvirtualenv 명령어를 사용할 경우 홈 디렉토리의 .virtualenvs폴더 안에 가상환경이름을 가진 폴더(test_env3)가 생깁니다.

  • 가상환경 지우기
1
2
3
$ rmvirtualenv 가상환경이름
# 예시
# $ rmvirtualenv test_env3

rmvirtualenv 명령어를 사용할 경우 mkvirtualenv로 만든 가상환경을 지워줍니다.

만든 가상환경을 지우는 방법은 이방법 뿐 아니라 홈 디렉토리의 .virtualenvs폴더 안의 가상환경이름을 가진 폴더를 지우는 방법도 있습니다.

  • 가상환경 진입하기 + 가상환경 목록 보기
1
2
3
4
5
6
$ workon 가상환경이름
# 가상환경으로 진입시 앞에 (가상환경이름)이 붙습니다.
(가상환경이름) $
# 예시
# $ workon test_env3
# (test_env3) $

workon명령어를 통해 mkvirtualenv로 만든 가상환경으로 진입할 수 있습니다.

workon명령어를 가상환경이름 없이 단순하게 칠 경우, 현재 만들어져있는 가상환경의 전체 목록을 불러옵니다.

1
2
$ workon
test_env3
  • 가상환경 빠져나오기
1
2
3
4
5
(가상환경이름) $ deactivate
$
# 예시
# (test_env3) $ deactivate
# $

가상환경에서 빠져나오는 것은 다른것들과 동일하게 deactivate명령어로 빠져나올 수 있습니다.

Windows의 경우

[DjangoTDDStudy] #02: UnitTest 이용해 기능 테스트 하기

최소기능 앱

TDD를 하는데 있어 가장 기본은 당연히 테스트코드를 짜는 것이다. 하지만 그 전에 먼저, 테스트 시나리오를 작성해야 한다. 예를들어, “인터넷 쇼핑몰에 들어간 후, 상품을 검색하고, 상품을 선택하고, 장바구니에 담고, 카트에 간 후, 결제를 한다.”라는 시나리오도 가능하다.

그리고, 이 시나리오를 충족하면서 가장 간단한 기능으로만 구성되지만 실제로 ‘동작’하는 앱을 만드는 것이다.

일단 이 최소기능 앱을 만들기 전에 우리 코드를 약간 바꿔보자.

Python 기본 라이브러리: unittest

지금까지는 selenium의 webdriver만을 직접 이용해왔지만, 만약 테스트 할 내용이 단순히 인터넷 창 하나를 띄우는것이 아니라 여러가지로 테스트를 늘려야 한다면 이전시간에 짠 코드는 딱히 도움이 되지는 않을 듯 하다.

그리고, 열려진 브라우저를 일일히 닫아주는 것도 상당히 귀찮다. 그러니까 unittest를 이용해 보자.

방금까지 사용한 functional_tests.py파일을 아래와 같이 바꿔보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# functional_tests.py

from selenium import webdriver
import unittest

class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome('chromedriver')

def tearDown(self):
self.browser.quit()

def test_can_start_a_list_and_retrieve_it_later(self):
self.browser.get('http://localhost:8000')

self.assertIn('To-Do', self.browser.title)
self.fail('Finished the Test')

if __name__=='__main__':
unittest.main(warning='ignore')

위 코드중 마지막의 if __name__=='__main__'은, 이 파이썬 코드가 다른 파이썬 파일에서 import 되어 사용되지 않고 python functional_tests.py라고 직접 실행한 경우에만 아래 코드를 실행한다.

만약 위 파일이 import functional_tests의 방식으로 import되었다면 위 코드의 __name__은 functional_tests가 된다.

1
2
3
4
5
6
7
8
from abc import some_thing
# __name__ 은 some_thing

import abc
# __name__ 은 abc

from abc import some_thing as st
# __name__ 은 st

이와 같은 __name__을 가진다.

다시한번 위 코드를 살펴보면, NewVisitorTest 클래스는 unittest라이브러리의 TestCase클래스를 상속받고 내장함수는 setUp, teatDown, test_can_start_a_list_and_retrieve_it_later가 있다.

unittest.main()을 통해 실행되면 ~~Test라는 class들이 모두 테스트 클래스로 지정되고 실행되는데, 이 클래스 안에 있는 test_로 시작하는 함수 하나하나가 테스트 함수로 인식된다. (만약 test_로 시작하지 않으면 테스트 코드라고 인식하지 않는다.)

또한, 테스트 함수 하나하나가 실행되기 전에 setUp이 실행되고 테스트 함수가 끝날때 마다 tearDown이 실행된다.

암묵적 대기 / 명시적 대기

셀레늄이 URL에 들어가 페이지 로딩이 끝날때까지 기다렸다가 동작하기는 하지만, 완벽하지는 않기때문에 (ajax call로 DOM을 재구성하는 경우 등) 얼마정도 우리가 지정한 element가 나올때까지 기다리게 할 수 있다.

바로 implicit_wait()이라는 암시적 대기를 주는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# functional_tests.py

from selenium import webdriver
import unittest

class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome('chromedriver')
self.browser.implicit_wait(3)

def tearDown(self):
self.browser.quit()

def test_can_start_a_list_and_retrieve_it_later(self):
self.browser.get('http://localhost:8000')

self.assertIn('To-Do', self.browser.title)
self.fail('Finished the Test')

if __name__=='__main__':
unittest.main(warning='ignore')

browser에 implicit_wait을 3초로 두었다.
그런데, 이 implicit_wait은 이 테스트 코드 전체 행동에 영향을 준다. 즉, 어떤 동작을 하기 전에 일단 3초는 허용하고 본다는 것이다.

그래서 좀 더 복잡한 코드의 경우에는 명시적 대기를 줘야만 한다.

아래는 selenium의 공식 문서 webdriver_advanced의 코드를 일부 변형한 것이다.(Driver/URL)

1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait # v2.4.0 이상
from selenium.webdriver.support import expected_conditions as EC # v2.26.0 이상

browser = webdriver.Chrome('chromedriver')
browser.get("http://localhost:8000")
try:
element = WebDriverWait(browser, 10).until(EC.presence_of_element_located((By.ID, "myDynamicElement")))
finally:
browser.quit()

위 코드에서 브라우저는 0.5초마다 ‘myDynamicElement’라는 ID를 가진 요소가 존재하는지를 체크한다. 만약 10초 내로 나타난다면 정상적으로 진행되고, 나타나지 않으면 TimeoutException을 내뱉는다.

여담

하지만 selenium문서에서는 이 코드는 implicit_wait와 크게 다르지 않다고 한다. 좀더 공부가 필요할 듯 하다.

[DjangoTDDStudy] #01: 개발환경 세팅하기(Selenium / ChromeDriver)

Web을 직접 테스트한다고?

웹 서비스를 개발하는 과정에서 꼭 필요한 것이 있다. 바로 실제로 기능이 동작하는지 테스트 하는 것.
이 테스트를 개발자가 직접 할 수도 있고, 혹은 전문적으로 테스트만 진행하는 QA팀에서 진행할 수도 있다.
하지만 위의 두 방법은 ‘사람이 직접 해야한다’는 공통점이 있다. 이걸 자동화 할 수 있다면 어떨까?

Selenium 설치하기

Selenium은 위의 질문에 대한 답변을 준다. 사람이 하기 귀찮은 부분을 자동화!

우선 Selenium을 설치해주자.
(단, Python3가 설치되어있다는 상황을 가정하며, Virtualenv / Pyvenv등의 가상환경 사용을 권장한다. 이 게시글에서는 tdd_study라는 이름의 가상환경을 이용한다.)

1
$ pip install selenium

PIP가 설치되어있다면 위 명령어 한줄만으로 Selenium이 설치된다.
Selenium의 설치가 완료되었다면, 우선 ChromeDriver를 받아준다.

ChromeDriver 설치하기

Selenium은 기본적으로 Firefox 드라이버를 내장하고있다. 이 ‘Driver’들은 시스템에 설치된 브라우저들을 자동으로 동작하게 하는 API를 내장하고 있고, 우리는 각 브라우저별 드라이버를 다운받아 쉽게 이용할 수 있다.

현재 Selenium은 대다수의 모던 웹브라우저들(Chrome, Firefox, IE, Edge, Phantomjs, etc.)을 지원하고 있기 때문에, 일상적으로 사용하는 크롬드라이버를 사용하기로 했다.
(만약 Headless Browser를 이용해야 한다면 Phantomjs를 이용해보자.)

크롬드라이버는 크로미움의 ChromeDriver에서 최신 버전으로 받을 수 있고, 이번 스터디에서는 Chrome v54에서 v56까지를 지원하는 ChromeDriver 2.27버전을 이용하려 한다.

크롬드라이버는 어떤 파일을 설치하는 것이 아니라, Binary가 내장되어있는 하나의 실행파일이라고 보면 된다.

만약 MAC OS나 Linux계열을 사용한다면, 크롬드라이버를 받은 후 그 파일을 PATH에 등록해 주자.

예시)
위 사이트에서 받은 파일의 이름이 chromedriver 이고 받은 경로가
/Users/beomi/bin 이라면,
사용하는 쉘(bash / zsh등)의 RC파일(유저 홈 디렉토리의 .bashrc / .zshrc)의 제일 아래에

1
export PATH=${PATH}:~/bin

위의 코드를 적고 저장한 후, 쉘을 재실행해준다.(터미널을 껐다가 켜주자.)

이렇게 하고나면, 아래 실습시 크롬드라이버의 위치를 지정하지 않고 파일 이름만으로 이용 할 수 있다는 장점이 있다.

Django 설치하기

Django는 앞으로 우리가 스터디에 사용할 WebFramework다.

1
$ pip install django

위 명령어로 역시 쉽게 설치 가능하다.
(2016.12.27기준 1.10.4가 최신버전이며, 1.10.x버전으로 스터디를 진행할 예정이다.)

pip로 Django가 설치되고 나면 django-admin 이라는 명령어를 쉘에서 사용할 수 있다.

실습

0. 설치 잘 되었는지 확인해 보기

쉘에서

1
$ pip list --format=columns

라는 명령어를 쳤을 때 아래 스샷과 같이 Django와 selenium이 보인다면 정상적으로 설치가 진행 된 것이다.

설치가 잘 되었다면 다음으로 진행해 보자.

1. Selenium 이용해보기

1
2
3
4
5
6
7
8
from selenium import webdriver

browser = webdriver.Chrome('chromedriver')
# chromedriver가 Python파일과 같은 위치에 있거나, 혹은 OS의 PATH에 등록되어 쉘에서 실행 가능한 경우 위와같이 한다.
# 혹은 browser = webdriver.Chrome('/path/to/chromedriver')의 절대경로로 해도 된다.
browser.get('http://localhost:8000')

assert 'Django' in browser.title

위 코드는 Chrome 브라우저를 작동시키는 WebDriver를 이용해 새 크롬 창을 띄우고 http://localhost:8000이라는 url로 들어간 후 브라우저의 title에 ‘Django’라는 글자가 들어가 있는지를 확인(Assert)해준다.

현재 상황에서는 django웹서버를 실행하지 않았기 때문에 당연하게도 AssertionError가 난다.

2. Django 서버 띄우기

이제 Django서버를 띄워보자.

Django는 django-admin이라는 명령어를 통해 기본적인 뼈대가 구성된 프로젝트 폴더 하나를 만들어 준다.

1
$ django-admin startproject tdd_study_proj

위 명령어를 치면 다음과 같은 폴더 구조를 가진 프로젝트 폴더가 생긴다.

1
2
3
4
5
6
7
8
9
10
(tdd_study) ➜  tdd_study_proj tree
.
├── manage.py
└── tdd_study_proj
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

1 directory, 5 files

(유의: tree명령어는 Mac OS에서 HomeBrew를 통해 설치한 패키지다. 자신의 쉘에서 동작하지 않는다고 문제가 있는건 아니다.)

위 파일 구조를 보면 tdd_study_proj라는 큰 폴더(현재위치) 안에 manage.py파일과 현재위치 폴더이름과 같은 tdd_study_proj라는 프로젝트 폴더가 생겨있다.

이 상태에서 장고에 내장된 테스트 웹서버를 구동해 보자. 테스트용 웹서버는 runserver 라는 명령어로 실행할 수 있고, CTRL-C로 작동을 멈추게 할 수 있다.
manage.py파일이 있는 곳에서 아래의 명령어를 쳐주자.

1
$ python manage.py runserver

위 명령어를 치면 아래와 같이 테스트 서버가 http://127.0.0.1:8000 에서 실행되고 있다.
(참고: 127.0.0.1 주소는 localhost와 동일합니다. 즉, 127.0.0.1:8000은 localhost:8000입니다.)

위 URL로 들어갔을 때 아래와 같은 화면이 나온다면 Django가 정상적으로 설치되었고, 테스트 웹서버도 정상적으로 구동중인 것이다.

3. 다시한번 테스트!

Django서버가 켜져있는 상태로 둔 후, 새 쉘(혹은 cmd)창을 켜서 실습1. Selenium 이용해보기에서 만든 파일을 manage.py파일이 있는 폴더에 selenium_test.py라는 이름으로 만들어 주자.

# selenium_test.py

from selenium import webdriver

browser = webdriver.Chrome('chromedriver')
browser.get('http://localhost:8000')

assert 'Django' in browser.title

이제는 에러가 나지 않고 테스트가 아무말(아무 에러)없이 끝나는걸 볼 수 있다 :)

[DjangoTDDStudy] #00: 스터디를 시작하며

스터디를 시작하며

파이썬을 이용한 클린 코드를 위한 테스트 주도 개발이라는 도서를 처음 보았을 때는 Django와 관련이 있는 책이라고는 생각조차 하지 못했다. 단지, 파이썬을 좀 더 잘 하려면 어떤 것을 알아야 할까 하고U 생각하던 중 TEST를 사용하는 때가 생산성이 올라간다는 말을 듣고서 “테스트 주도 개발”을 하려고 시도해 보려던 참, 이 책을 보게 된 것이다.

Python/Django 공부에 관심을 가지고 있던 지환님과 이야기 하던 중, 마침 스터디에 대한 이야기가 나왔고 이 염소책과 TDD에 대한 이야기도 나왔다. 사실상 즉석에서 ‘아 그래요? 그러면 해보죠!’라는 느낌으로 시작했다. 그자리에서 명서님에게 물어보니 바로 동참하신다고 해서 9XD에 글을 바로 글을 올리고 스터디원을 모집했다.
사실 이정도로 인기가 많을거라고 생각하지는 못했다

생각보다 빠르게 열명이 찼고, 저번주 화요일에 첫 모임을 가졌다.

첫 모임에서

첫 모임에서는 많은 내용을 다루지는 않았다. 하지만 진행에 있어 미흡한 준비가 아쉬웠다.
모임 전 계획은 서로 인사하기 / 왜 우리는 여기에 왔는가 / 우리의 목표 등을 이야기하고, Django를 사용하지 않은 분들을 위해 개발환경을 설치하고, 1장 정도(Selenium을 이용해 크롬드라이버로 테스트 한번 돌려보기)를 진행하려 했다.
하지만 위 내용이 생각보다 빨리 진행되어 1시간내로 모든게 끝나버렸고, 남은 1시간동안은 뭘 해야할지 고민이 들어 2장을 조금 진행해 보려고 했는데 사실 이게 욕심이었다고 생각한다. 물론, 2~3단원까지 끝낸다면 진도상으로는 좋다고 말할 수 있지만 구성원들이 스터디를 따라오지 못할 수 있다고 겁을 먹을 수 있었기 때문이다.
다음에 스터디를 처음 시작한다면, 좀더 여유로운 시간을 가지고 ‘스터디’가 아닌 ‘모임’으로 첫 만남을 카페 등에서 가지면 더 좋지 싶다.

Fabric Put 커맨드가 No Such File Exception을 반환할 때 해결법

환경

1
2
Python 3.5+
Fabric3

문제 발생 상황

1
2
def _put_envs():
put('envs.json', '~/{}/envs.json'.format(PROJECT_NAME))

이와 같이 로컬에는 envs.json파일이 명확히 존재하고 있었다.
그러나 Fabric에서는

1
Fatal error: put() encountered an exception while uploading 'envs.json'

위와 같은 에러를 여전히 뿜고 있었다.

하지만 StackOverflow:Fabric put command gives fatal error: ‘No such file’ exception 게시글을 살펴보면 이 문제는 Fabirc의 에러 창이 잘못되었다는 것을 말해준다.

즉, 위 에러에서는 로컬 위치에 envs.json이 없다고 말하지만 실제로는 서버, 그러니까 '~/{}/envs.json'.format(PROJECT_NAME)에 해당하는 위치가 원격 서버 상에 존재하지 않아서 에러를 내는 것이다.

그래서 Fabric코드의 순서를 바꾸어 주었다.

기존 순서가

1
2
3
4
5
6
7
8
9
10
def _put_envs():
put(os.path.join(PROJECT_DIR, 'envs.json'), '~/{}/envs.json'.format(PROJECT_NAME))

def _get_latest_source():
if exists(project_folder + '/.git'):
run('cd %s && git fetch' % (project_folder,))
else:
run('git clone %s %s' % (REPO_URL, project_folder))
current_commit = local("git log -n 1 --format=%H", capture=True)
run('cd %s && git reset --hard %s' % (project_folder, current_commit))

와 같이 envs를 업로드 후 github소스를 받아오는 것이었다면, 이제는 소스를 먼저 가져온 후 (_get_latest_source를 먼저 실행 후) envs를 업로드 하도록 바꾸었다.

이 경우 정상적으로 실행 되었다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×