์ค๋ ํ๋ฃจ ํด์ผํ ์ผ๊ณผ ์์๋ ์ผ์ ์ฝ๊ณ ์์๊ฒ ๊ธฐ๋กํ ์ ์๋ ์ดํ๋ฆฌ์ผ์ด์
์ฃผ์ ๊ธฐ๋ฅ ์ ๋ฆฌ
- ๋ชฉํ ์ค์ , ๋ชฉํ๋ณ ์์ ๊ด๋ฆฌ
- ๋ชฉํ ๋น ํ ์ผ ๋ชฉ๋ก ๋ถ๋ฅ
- ํ ์ผ ๋ณด๊ดํจ์ผ๋ก ์ด๋
- ์ํ๋ ์๊ฐ์ ํ ์ผ ์๊ฐ ์๋ฆผ
- ์ค๋ ํ๋ฃจ ์ผ๊ธฐ ์ ๊ธฐ
- ์ผ๊ธฐ์ ๋ํ ์ด๋ชจ์ง ์ค์
- ์น๊ตฌ ๊ณ์ ํ๋ก์
-
todo์ color๋ diary์ emoji๋ ๊ฐ๋ฐํ๊ฒ ๋๋ค๋ฉด ํ๋ก ํธ ์ธก๊ณผ ํฉ์ํด์ ๊ฒฐ์ ํด์ผ ํ ๊ฒ ๊ฐ๋ค.
-
follower/following์ ์ค๊ณ๊ฐ ์ ๋ฐ ์์ด ์๋ ๊ฒ ๊ฐ๋ค.
models.py ์์ฑ ๋๋๋ฉด migration!
python manage.py makemigrations
python manage.py migrate
python shell ๋ค์ด๊ฐ๊ธฐ
python manage.py shell
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํด๋น ๋ชจ๋ธ ๊ฐ์ฒด 3๊ฐ ๋ฃ๊ธฐ
- ์ฝ์
ํ ๊ฐ์ฒด๋ค์ ์ฟผ๋ฆฌ์
์ผ๋ก ์กฐํํด๋ณด๊ธฐ (๋จ, ๊ฐ์ฒด๋ค์ด ๊ฐ์ฒด์ ํน์ฑ์ ๋ํ๋ด๋ ๊ตฌ๋ถ๊ฐ๋ฅํ ์ด๋ฆ์ผ๋ก ๋ณด์ฌ์ผ ํจ)
- filter ํจ์ ์ฌ์ฉํด๋ณด๊ธฐ
-
ModuleNotFoundError: No module named 'environ'
$ pip install django-environment
-
NameError: name '_mysql' is not defined
django์์ mysql ๊ฐ๋ฐํ ๋ ๊ฐ๋ ๋ฐ์ํ๋ ์๋ฌ๋ผ๊ณ ํ๋ค. mysql์ reinstallํด๋ณด๊ณ ์๋๋ค๋ฉด ์๋์ ๊ฐ์ด pymysql๋ก ์ถฉ๋ ๋ฐ ํธํ ๋ฌธ์ ์ก๊ธฐ!
$ pip install pymysql
setting.py ์ญํ ์ ํ๋ settings/base.py์ ์๋ ์ฝ๋ ์ถ๊ฐ
import pymysql pymysql.install_as_MySQLdb()
-
RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods
$ pip install cryptography
-
MySQL django.db.utils.OperationalError : (1045, " 'root'@ 'localhost'์ฌ์ฉ์์ ๋ํ ์ก์ธ์ค๊ฐ ๊ฑฐ๋ถ๋์์ต๋๋ค (์ํธ ์ฌ์ฉ : YES)")
settings.py์ DATABASE_PASSWORD ์ฌํ์ธ
-
OneToOneField
์ผ๋์ผ ๊ด๊ณ๋ก unique=True๋ฅผ ์ด์ฉํด์ ๋ง๋ ForeignKey์ ๋น์ทํ์ง๋ง ๋จ์ผ ๊ฐ์ฒด๋ฅผ ์ง์ ๋ฆฌํดํ๋ ์ญ์ฐธ์กฐ๋ผ๋ ์ ์ด ๋ค๋ฅด๋ค.
-
DateField์์ default=datetime.date.today()๋ฅผ ์ผ๋๋ ์๋์ ๊ฐ์ ๊ฒฝ๊ณ ๊ฐ ๋ํ๋ฌ๋ค.
It seems you set a fixed date / time / datetime value as default for this field. This may not be what you want. If you want to have the current date as default, use `django.utils.timezone.now`
๊ทธ๋์ from django.utils import timezone๋ฅผ ์ถ๊ฐํด์ timezone.localtime() ํ์์ผ๋ก ๋ฐ๊พธ๊ธด ํ๋๋ฐ ์ ์ด๋ ๊ฒ ํด์ผ ํ๋์ง ์ ๋ชจ๋ฅด๊ฒ ๋ค.
DB ์ค๊ณ๋ฅผ ๋๋ฌด ์ค๋๋ง์ ํด๋ด์ ๊ฐ์ด ์ ์์กํ๋ค. create/update ์๊ฐ๋ ํ๋๋ก ์ถ๊ฐํ์ด์ผ ํ๋๋ฐ ์์๋ค. ๊ทธ๋ฆฌ๊ณ following/follower๋ฅผ ๊ตฌ์กฐ ์ ์ด๋ป๊ฒ ํํํด์ผ ํ ์ง ๋ชจ๋ฅด๊ฒ ์ด์ ๋ด ์๊ฐ๋๋ก ํด๋ดค๋๋ฐ ์๋ง ํ๋ฆฐ ๊ฒ ๊ฐ๋ค. ๐ฅฒ ์ฑ ์ข ์ฝ๊ณ ๊ณต๋ถํด์ผ๊ฒ ๋ค!
2์ฃผ์ฐจ ๊ณผ์ ์ฝ๋๋ฆฌ๋ทฐ ๋ ๋ง์ํด์ฃผ์ จ๋ ์ ๋ค์ ๋ฐ์ํ์ฌ ๊ตฌ์กฐ๋ฅผ ์์ ํ๋ค.
class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(null=True, default=None)
is_deleted = models.BooleanField(default=False)
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
BaseModel Class๋ฅผ ๋ง๋ค์ด ๋ค๋ฅธ ๋ชจ๋ธ์์๋ ๋ฐ๋ณต์ ์ผ๋ก ํ์ํ ๋ณ์๋ค์ ์ถ๊ฐํ์ฌ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๋ค.
-
์ถ๊ฐ๋ ๋ฐ์ดํฐ
- Category: study, play
- Todo: django study, code review, lets go sinchon
-
mysql๋ก ํ์ธ
-
URL: api/todo
-
METHOD: POST
-
BODY
{ "user": "์ ์ ID", "category": "์นดํ ๊ณ ๋ฆฌ ID", "content": "TODO ๋ด์ฉ" }
deadline์ ์ง์ ํ์ง ์์๋ ๊ด์ฐฎ์ง๋ง models.py์์ field์ default์ ๋ฐ์ดํฐ ํ์ ์ ๋ค๋ฅด๊ฒ ์ค์ ํ์ฌ ์๋ฌ๊ฐ ๋ ์ด๋ฒ์๋ง ์ค์ ํด์ฃผ์๋ค. ์ถํ์ ์์ ์์
-
URL: api/todo/< int:pk >
-
METHOD: POST
-
BODY
{ "user": "์ ์ ID", "category": "์นดํ ๊ณ ๋ฆฌ ID", "์์ ์ ์ํ๋ ํ๋" }
user์ category๋ฅผ body ์ถ๊ฐํ์ง ์๊ณ api๋ฅผ ์์ฒญํ์๋๋ ํ์๊ฐ์ด๋ผ๊ณ ์๋ฌ๊ฐ ๋ฌ๋ค. ์ํด๋ ์๊ด ์๋ ๊ฒ์ผ๋ก ์๋๋ฐ ํ์ธ ํ์!
serializer = TodoSerializer(instance=todo, data=data, partial=True)
serializer์ partial=True์ ์ถ๊ฐํ์ฌ ํด๊ฒฐ
-
BaseModel์ created_at
์ด๋ created_at์ ๊ทธ๋ฅ auto_now_add=True๋ง ์ง์ ํด์ฃผ๋ฉด ๋ค์๊ณผ ๊ฐ์ด default๋ฅผ ์ถ๊ฐํ๋ผ๋ ๋ฉ์์ง๊ฐ ๋์จ๋ค.
๊ทธ๋์ default๋ฅผ ์ง์ ํด์ฃผ๋ฉด ๋ ์ค์ ํ๋๋ง ์ฐ๋ผ๊ณ ์๋ฌ ๋ฉ์์ง๊ฐ ์ถ๋ ฅ๋์ด null=True์ ์ถ๊ฐํ์ฌ ์ฐ์ ํด๊ฒฐํด์ฃผ์๋ค.
-
DELETE DELETE ์์ฒญ ์์ ๋ฐ์
TypeError: __init__() missing 1 required positional argument: 'data'
์๋ฌ๊ฐ ๋์ง๋ง DB๋ฅผ ํ์ธํด๋ณด๋ฉด ์ด์ฐ๋๋ ์ง์์ ธ ์์๋ค. ๊ตฌ๊ธ๋งํด๋ด๋ ์ ๋ชจ๋ฅด๊ฒ ์ด์ ๋ ์ฐพ์๋ณด๊ณ ์์ ํด์ผ ํ๋ค.
โ JsonResponse๋ฅผ Response๋ก ์์ ํ์ฌ ํด๊ฒฐ!
-
safe
TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False.
GET ์์ฒญ ์์ ์๊พธ ๋ฐ์ํ๋ ์๋ฌ์ด๋ค. views.py์์ ๊ฐ api์ ๋ฆฌํด ๊ฐ์ safe๋ฅผ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.
return JsonResponse(serializer.data, safe=False)
๋ฐฑ์๋ ๊ฐ๋ฐ์๊ฐ ๋ ๊ธฐ๋ถ! ๋๋ฌด ์ฌ๋ฐ์๋ค ๐ค ์ฒ์์ urls.py์ ๋ด๊ฐ ์ง todo path๋ฅผ ์ถ๊ฐํด์ฃผ๋ ๊ฒ์ ์์ url conf์์ ํ๊ณ ์์๋ค. ์ด๋ฐ ๋ฐ๋ณด ๊ฐ์ ์ค์๋ ๋๋์ฒด ์ธ์ ๋๋๋๊ฑด์ง..
๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ณธ ํ ์ด๋ธ์ ๋ง๋ค์ด์ ์์ ๋ฐ๋ ๊ด๊ณ๋ก ๋ณ๊ฒฝํ๊ณ ๋์ ์ฝ๋๋ฅผ ์ง๋ ค๊ณ ํ๋๊น serializer์์๋ ์ด๋ป๊ฒ ํด์ผ ํ๋ ๊ฒ์ธ๊ฐ ๊ณ ๋ฏผ์ด ์์๋ค. ๊ทธ๋ฆฌ๊ณ ์ธ์ ๋ ์๋ ค์ฃผ์ SerializerMethodField๋ฅผ ์ถ๊ฐํด์ ์ข ํ๊ณ ์ถ์๋๋ฐ ์๋ฌ๊ฐ ์๊ฒจ์ ์ฐ์ ์ฃผ์์ฒ๋ฆฌ ํด๋จ๋ค.๐ข
์ด๋ฒ ๊ณผ์ ์์ ๋ชจ๋ฅด๋ ๋ถ๋ถ๋ค์ ๋ง์ด ๋ฐ๊ฒฌํด์ ๋ต๋ตํ๊ธฐ๋ ํ์ง๋ง ๊ณต๋ถํ ๊ฒ๋ค์ ์ฐพ์ ๊ฒ ๊ฐ์ ์ข์๋ค!
- url ํํ: todo/ โ todos/
- ํน์ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ๋ฉ์๋: PUT โ PATCH
- BaseModel์์ ์ญ์ ์ฌ๋ถ์ ์๊ธฐ๋ฅผ ๊ด๋ฆฌํ๋ is_deleted์ deleted_at ํ๋ ์ค is_deleted ์ ๊ฑฐ
- migration ํ์ผ๋ค git์ ์ถ๊ฐ
๊ธฐ์กด์ FBV(Function-Based View)๋ก ์ฝ๋ฉํ๋ ๋ด์ฉ์ CBV(Class-Based View)๋ก ์์ ํ์๋ค.
views.py refactoring ์ /ํ
# FBV
@csrf_exempt
@api_view(['GET', 'POST'])
def todo_list(request):
if request.method == 'GET':
todos = Todo.objects.filter(deleted_at=None)
serializer = TodoSerializer(todos, many=True)
return JsonResponse(serializer.data, safe=False, status=200)
# CBV
class TodoList(APIView):
def get(self, request):
todos = Todo.objects.filter(deleted_at=None)
serializer = TodoSerializer(todos, many=True)
return JsonResponse(serializer.data, safe=False, status=200)
urls.py refactoring ์ /ํ
# FBV
urlpatterns = [
path('todos/', views.todo_list, name="todo_list"),
path('todos/<int:pk>', views.todo_detail, name="todo_detail"),
]
# CBV
urlpatterns = [
path('todos/', TodoList.as_view()),
path('todos/<int:pk>', TodoDetail.as_view()),
]
views.py refactoring ํ
class TodoViewSet(viewsets.ModelViewSet):
serializer_class = TodoSerializer
queryset = Todo.objects.all()
urls.py refactoring ํ (Router ์ฌ์ฉํ์ฌ url mapping)
router = routers.DefaultRouter()
router.register(r'todos', TodoViewSet)
urlpatterns = router.urls
- ํน์ user filtering
- content์ ํน์ ๋ฌธ์์ด ํฌํจ๋๋์ง ํ๋ณํ์ฌ filtering
class TodoFilter(FilterSet):
user = filters.CharFilter(method='user_filter')
content = filters.CharFilter(field_name='content', lookup_expr='icontains')
class Meta:
model = Todo
fields = ['user', 'content']
def user_filter(self, queryset, user, value):
filtered_queryset = queryset.filter(**{
user: value,
})
return filtered_queryset
class TodoViewSet(viewsets.ModelViewSet):
serializer_class = TodoSerializer
queryset = Todo.objects.all()
filter_backends = [DjangoFilterBackend]
filterset_class = TodoFilter
url: /api/todos/?user=''
url: /api/todos/?content=''
url: /api/todos/?user=''&content=''
-
Field ์ญ์ ์๋ฌ
is_deleted ํ๋๋ฅผ ์ญ์ ํ๊ณ deleted_at์ผ๋ก๋ง ์ญ์ ์ฌ๋ถ์ ์๊ธฐ๋ฅผ ๊ด๋ฆฌํ๋๋ก models.py๋ฅผ ์์ ํ์๋ค. ํ์ผ ์์ ํ์ ๋ง์ด๊ทธ๋ ์ด์ ์ ํ๋๋ฐ๋ DB์๋ ๋ฐ์์ด ๋์ง ์์ ์์ง ํ๋๊ฐ ๋จ์์์ด ๋ฐ์ํ๋ ์ค๋ฅ์๋ค. mysql๋ก ๋ค์ด๊ฐ
ALTER TABLE
๋ก ํ๋๋ฅผ ํ๋ํ๋ ์ญ์ ํ์ฌ ํด๊ฒฐํ ์ด๋ธ๋ช
DROP์ปฌ๋ผ๋ช
; -
Todo TypeError
ํน์ ๋ฐ์ดํฐ๋ฅผ ํ์ธํ ๋ ๋ฐ์ํ๋ ์๋ฌ๋ก get_object_or_404๋ฅผ objects.filter๋ก ์์ ํ์ฌ ํด๊ฒฐ
๊ณผ์ ํ๋ ค๊ณ ๋ณด๋๊น ๋ถ๋ช ์์์ผ๊น์ง๋ง ํด๋ ์๋ migration file๋ค์ด ๋ค ๋ ๋ผ๊ฐ์ ๊ฐ๋ด์ด ์๋ํ๋ค. git์ migration file๋ค์ ๊ตณ์ด ์ฌ๋ฆด ํ์๊ฐ ์๋..? ์ถ์ด์ ์์ฌ๋ ธ์๋๋ฐ ์ด์ ๊ผฌ๋ฐ๊ผฌ๋ฐ ์ฌ๋ ค์ผ๊ฒ ๋ค. ํ์ผ๋ค์ด ๋ค ๋ ๋ผ๊ฐ์ด๋ DB ์ฐ๊ฒฐ์ ์ ๋์ด์๊ณ migration ๊ธฐ๋ก๋ค์ ๋ณด๋ฉด ์์ง ๋ค ์๋๋ฐ ์ ๋ด ๋ก์ปฌ์์๋ง ์ฌ๋ผ์ง๊ฑด์ง ์ ๋ง ์๋ฌธ ๐ค ๊ทธ๋ฆฌ๊ณ ํผ๋๋ฐฑ์ ๋ฐ๊ณ ์ ์ฝ๋๋ฅผ ์์ ํ๋ ๋ถ๋ถ๋ค์ด ์์์น ๋ชปํ๊ฒ ์๋ฌ๊ฐ ๋์ ์ ๊ทธ๋ฌ๋๊ฑด์ง๋ ๊ฐ์ด ์์กํ๋ค. ์ฐ์ ์ฃผ๋จน๊ตฌ๊ตฌ์์ผ๋ก ํด๊ฒฐ..
CBV์ ViewSet ๋ชจ๋ ์ฒ์ ์ฌ์ฉํด๋ณด๋๋ฐ ์ ๋ง ์ ์ธ๊ณ์๋ค. ํนํ ViewSet ์ด๋ป๊ฒ ์ด๋ ๊ฒ ๊ฐํธํ ์๊ฐ..! ๊ทผ๋ฐ ์คํ๋ ค ์ฒ์ ๋ฐฐ์ธ ๋ ViewSet์ผ๋ก ํ์ผ๋ฉด ์ด๋ป๊ฒ ์๋ํ๋๊ฑด์ง ๋ชฐ๋ผ์ ํท๊ฐ๋ ธ์ ๊ฒ ๊ฐ๋ค. filterset๋ ์ต์ํ์ง๊ฐ ์์์ deleted_at์ด Null์ด ์๋ ๋ฐ์ดํฐ๋ค๋ง ๊ฐ์ ธ์ค๋ ํํฐ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ณ ์ถ์๋๋ฐ ๋ง๋ค๋ค๊ฐ ํฌ๊ธฐํ๋ค ๐ ์ด์จ๋ ๋๋ฌด๋๋ฌด ํธํ ๊ธฐ๋ฅ๋ค์ ์๊ฒ ๋์ด์ ์ฌ๋ฐ์๋ค!
+) ์ธ์ฆ์ ํด์ผํ๋ ์ด์
HTTP๋ ๊ธฐ๋ณธ์ ์ผ๋ก stateless, connectionlessํ๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ์์ฒญ(Request)์ด ์ด์ ์์ฒญ๊ณผ ๋ ๋ฆฝ์ ์ผ๋ก ๋ค๋ค์ง๋ค. ์์ฒญ์ด ๋๋ ๋๋ง๋ค ์๋ฒ๋ ์ ์ ์ ๋ํ ์ ๋ณด๋ฅผ ์์ด๋ฒ๋ฆฌ๊ฒ ๋๊ธฐ ๋๋ฌธ์ ์์ฒญ ์๋ง๋ค ํด๋ผ์ด์ธํธ๋ ์๋ฒ์ ์ธ์ฆ์ ํด์ผ ํ๋ค.
HTTP Request Header์ ์ธ์ฆ ์๋จ์ธ ๋น๋ฐ๋ฒํธ๋ฅผ ์ง์ ๋ฃ๋ ๋ฐฉ์์ด๋ค. ๋ณดํต ์๋ฒ๋ก HTTP ์์ฒญ์ ํ ๋ ์ํธํ๋ฅผ ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ณด์์ ์ผ๋ก ๋งค์ฐ ์น๋ช ์ ์ด๋ค. ๋ง์ฝ ํด์ปค๊ฐ HTTP ์์ฒญ์ ๋ณผ ์ ์๋ค๋ฉด ์ฌ์ฉ์์ ๊ณ์ ์ ๋ณด๋ฅผ ์ฝ๊ฒ ์ ์ ์๋ค.
- ์ฅ์
- ์ธ์ฆ ํ ์คํธ ๋ ์ฌ์ฉ ๊ฐ๋ฅ
- ๋จ์
- ๋ณด์ ๋งค์ฐ ์ทจ์ฝ
- ์์ฒญ ์๋ง๋ค ์๋ฒ์ ID, PW ๋์กฐ ํ์
*Session: ์๋ฒ๊ฐ ๊ฐ์ง๊ณ ์๋ ์ ๋ณด *Cookie: ์ฌ์ฉ์์๊ฒ ๋ฐ๊ธ๋ ์ธ์ ์ ์ด๊ธฐ ์ํ ์ด์ (Session ID)
Session, Cookie ๋ฐฉ์์ Session ID๋ฅผ ๋ง๋๋ ์ธ์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ด๋ค. Session ID๋ ๋ก๊ทธ์ธ์ ํ์ ๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๊ฒ์ผ๋ก HTTP Header์ ์ค๋ ค ์ฌ์ฉ์์๊ฒ ๋ณด๋ด์ง๋ค. ์ฌ์ฉ์๋ ๋ณด๊ดํ๊ณ ์๋ ์ฟ ํค๋ฅผ ์ธ์ฆ์ด ํ์ํ ์์ฒญ์ ๋ฃ์ด ๋ณด๋ด๊ณ ์๋ฒ๋ ์ธ์ ์ ์ฅ์์์ ์ฟ ํค์ ๊ธฐ์กด ์ ๋ณด๋ฅผ ๋น๊ตํ์ฌ ์ธ์ฆํ๋ค. ์ธ์ ์ ์ฌ์ฉํ์ฌ ์ธ์ฆํ์ฌ ์ฑ ์์ ์๋ฒ๊ฐ ์ง๊ฒ ํ๋ค๊ณ ๋ณผ ์ ์๋ค.(์ฌ์ฉ์๋ณด๋ค๋ ์๋ฒ ํดํน์ด ๋ ์ด๋ ต๊ธฐ ๋๋ฌธ)
- ์ฅ์
- Header ๋ฐฉ์๊ณผ๋ ๋ค๋ฅด๊ฒ HTTP ์์ฒญ์ด ๋ ธ์ถ๋๋๋ผ๋ ์์ ํ๋ค. ์ฌ์ฉ์์ ์ ๋ณด๋ ์ธ์ ์ ์ฅ์์ ์ ์ฅ๋๊ณ HTTP ์์ฒญ์ ๋ค์ด์๋ ์ฟ ํค ์์ฒด๋ ์ ์๋ฏธํ ์ ๋ณด๊ฐ ์๊ธฐ ๋๋ฌธ์ด๋ค.
- ์ฌ์ฉ์๋ ๊ฐ๊ฐ ๊ณ ์ ํ Session ID๋ฅผ ๋ฐ๊ธ ๋ฐ์ ํ์ ์ ๋ณด ํ์ธ์ด ๋งค๋ฒ ํ์ํ์ง ์๊ธฐ ๋๋ฌธ์ ์๋ฒ ์์์ ์ ๊ทผ์ด ์ฉ์ดํ๋ค.
- ๋จ์
- Session Hijacking ๊ณต๊ฒฉ ๊ฐ๋ฅ ์ธ์ ์ ๊ฐ๋ก์ฑ์ ๋ณ๋์ ์ธ์ฆ ์์ ์์ด ์ธ์ ์ ํตํด ํต์ ์ ๊ณ์ํ๋ ํ์๋ฅผ ๋งํ๋ค. HTTPS ํ๋กํ ์ฝ์ ์ฌ์ฉํ๊ฑฐ๋ ์ธ์ ์ ๋ง๋ฃ ์๊ฐ์ ์ค์ ํ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ๋ค.
- ์ธ์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ์ ์ฅ๊ณต๊ฐ์ด ํ์ํ๋ค.
- ์ฅ์
- ์ธ์ ์ฟ ํค ๋ฐฉ์๊ณผ ๋ฌ๋ฆฌ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ์ ์ฅ๊ณต๊ฐ์ด ํ์ํ์ง ์๋ค.
- Google, Facebook๊ณผ ๊ฐ์ ๋ค์ํ ํ ํฐ ๊ธฐ๋ฐ ์๋น์ค๋ก ๊ด๋ จ ๊ธฐ๋ฅ์ ํ์ฅํ๊ธฐ ์ฉ์ดํ๋ค
- ์๋ช ์๋ ์ก์ ์์ ์ก์ ํ ์ ๋ณด๋ค์ ๋ํ ๋ด์ฉ์ด ํฌํจ๋์ด ์์ด ์๋ฒ์์ ๋ฐ์ดํฐ ์กฐ์ ๋ฐ ๋ณ์กฐ ์ฌ๋ถ๋ฅผ ์์๋ผ ์ ์๋ค.
- ๋จ์
- Token์ด ๋ฐ๊ธ๋๋ฉด ๋ง๋ฃ ์๊ฐ ์ ๊น์ง ๊ณ์ ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ ์ธ์ ์ฟ ํค ๋ฐฉ์๊ณผ ๊ฐ์ด ํด์ปค๊ฐ ํ ํฐ์ ๊ฐ๋ก์ฑ์ ์ฌ์ฉํ ์ ์๋ค. Refresh Token์ ๋ฐ๊ธํ์ฌ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ๋ค.
- Payload๋ ๋ฐ๋ก ์ํธํํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ด์ ์ ์๋ ์ ๋ณด๊ฐ ์ ํ์ ์ด๋ค.
- Token์ ๊ธธ์ด๊ฐ ๊ธธ์ด ์์ฒญ์ด ๋ง์์ง์๋ก ์๋ฒ์ ์์ ๋ญ๋น๊ฐ ์๊ธด๋ค.
*Refresh Token: Access Token๊ณผ ๊ฐ์ ํํ์ JWT์ด๋ค. Access Token๋ณด๋ค ๊ธด ์ ํจ๊ธฐ๊ฐ์ ๊ฐ์ง๋ฉฐ Access Token ๋ง๋ฃ ์์ ์๋ก ๋ฐ๊ธ์ ๋์์ค๋ค.
Refresh Token์ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์์ฃผ ๋ก๊ทธ์ธ์ ํด์ผ ํ๋ ์ํฉ์ด๋ ์ฅ๊ธฐ๊ฐ ๋ก๊ทธ์ธํ์ ๋ ๋ฐ์ํ๋ ๋ณด์์ ๋ฌธ์ ์ ๋ค์ ํด๊ฒฐํ์๋ค.
- ์ฅ์
- ์ ํจ ๊ธฐ๊ฐ์ด ๋ ์งง๊ธฐ ๋๋ฌธ์ Access Token๋ง ๋จ๋ ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ณด๋ค ๋ณด์์ ์ผ๋ก ๋์ฑ ์์ ํ๋ค.
- ๋จ์
- ๊ตฌํ์ด ๋ณต์กํ๋ค.
- ์๋ฒ์ ์์ ๋ญ๋น๊ฐ ์๊ธด๋ค.
*OAuth 2.0(Open Authorization): ์ธ์ฆ์ ์ํ ๊ฐ๋ฐฉํ ํ์ค ํ๋กํ ์ฝ
- ์ฅ์
- ์ง์ ํ์ฌ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์ ๋ ฅํ๋ ๊ฒ๋ณด๋ค ์์ ์ ์ด๋ค.
- ํ์ ์ ๋ณด๋ฟ๋ง ์๋๋ผ ๊ธฐํ API์ ๋ํ ์ ๋ณด์๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
- ๋จ์
- ๊ตฌํ์ด ๋งค์ฐ ๋ณต์กํ๋ค.
ํต์ ์์๊ฐ์ ์ ๋ณด๋ฅผ JSON ํ์์ ์ฌ์ฉํ์ฌ ์์ ํ๊ฒ ์ ์กํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ด๋ค. JWT๋ ์ผ๋ฐ์ ์ผ๋ก ์ธ์ฆ(Authentication)๊ณผ ๊ถํ๋ถ์ฌ(Authorization)์ ์ฌ์ฉ๋๋๋ฐ ์ด๋ ํ์ํ ์ ๋ณด๋ค์ ์ํธํ์ํจ JSON ํ ํฐ์ด๋ค. ์ธ์ฆ ์ ์ฐจ๋ฅผ ๊ฑฐ์ณ์ ์๋ฒ์์ JWT๋ฅผ ๋ฐ๊ธํด์ฃผ๋ฉด ์ด๋ฅผ ์ ๋ณด๊ดํ๊ณ ์๋ ํด๋ผ์ด์ธํธ๊ฐ API ์ฌ์ฉ๊ณผ ๊ฐ์ ๋์ ์๋ฒ์ JWT๋ฅผ ์ ์ถํ์ฌ ์ธ๊ฐ๋ฅผ ๋ฐ์ ์ ์๋ค.
JSON ๋ฐ์ดํฐ๋ฅผ Base64 URL-safe Encode ๋ฅผ ํตํด ์ธ์ฝ๋ฉํ์ฌ ์ง๋ ฌํํ ๊ฒ์ด๋ฉฐ, ํ ํฐ ๋ด๋ถ์๋ ์๋ณ์กฐ ๋ฐฉ์ง๋ฅผ ์ํด ๊ฐ์ธํค๋ฅผ ํตํ ์ ์์๋ช ๋ ๋ค์ด์๋ค. ๋ฐ๋ผ์ ์ฌ์ฉ์๊ฐ JWT ๋ฅผ ์๋ฒ๋ก ์ ์กํ๋ฉด ์๋ฒ๋ ์๋ช ์ ๊ฒ์ฆํ๋ ๊ณผ์ ์ ๊ฑฐ์น๊ฒ ๋๋ฉฐ ๊ฒ์ฆ์ด ์๋ฃ๋๋ฉด ์์ฒญํ ์๋ต์ ๋๋ ค์ค๋ค.
-
- Header
- alg: ์๋ช ์ํธํ ์๊ณ ๋ฆฌ์ฆ(ex: HMAC SHA256, RSA)
- typ: ํ ํฐ ์ ํ
- Payload ํ ํฐ์์ ์ฌ์ฉํ ์ ๋ณด์ ์กฐ๊ฐ๋ค์ธ Claim์ด ๋ด๊ฒจ์์ *Claim: key-value ํ์์ผ๋ก ์ด๋ฃจ์ด์ง ํ ์์ ์ ๋ณด
- Signature ์๊ทธ๋์ฒ์์ ์ฌ์ฉํ๋ ์๊ณ ๋ฆฌ์ฆ์ ํค๋์์ ์ ์ํ ์๊ณ ๋ฆฌ์ฆ ๋ฐฉ์(alg)์ ํ์ฉ ์๊ทธ๋์ฒ์ ๊ตฌ์กฐ๋ (ํค๋ + ํ์ด๋ก๋)์ ์๋ฒ๊ฐ ๊ฐ๊ณ ์๋ ์ ์ผํ key ๊ฐ์ ํฉ์น ๊ฒ์ ํค๋์์ ์ ์ํ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ์ํธํ
- Header
- Custom User Model ์ฌ์ฉ
# models.py
class User(AbstractBaseUser):
email = models.EmailField(max_length=30, unique=True)
nickname = models.CharField(max_length=10)
password = models.CharField(max_length=30)
introduce = models.CharField(max_length=200)
image = models.TextField(blank=True)
is_public = models.BooleanField(default=False)
search = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = 'email'
class Meta:
db_table = "User"
def __str__(self):
return self.nickname
@property
def is_staff(self):
return self.is_superuser
Django์ ๊ธฐ๋ณธ ์ ์ ๋ชจ๋ธ์์ AbstractBaseUser๋ฅผ ์์๋ฐ์ ์ปค์คํ ๋ชจ๋ธ๋ก ๋ณํ์์ผฐ๋ค. is_superuser๋ก ๊ด๋ฆฌ์ ์ฌ๋ถ๋ฅผ ํ์ธํ๋ฉฐ user, superuser๋ฅผ ์์ฑํ๋ ๋ฉ์๋๋ UserManager์ ์ถ๊ฐํ์๋ค.
-
ํ์๊ฐ์ ๊ตฌํ
# serializers.py class JoinSerializer(serializers.ModelSerializer): email = serializers.EmailField(required=True) password = serializers.CharField(write_only=True, required=True) password2 = serializers.CharField(write_only=True, required=True) class Meta: model = User fields = ('email', 'nickname', 'password', 'password2') def validate(self, request): if request['password'] != request['password2']: raise serializers.ValidationError({"Password doesn't match."}) return request def save(self, request): user = User.objects.create_user( email=self.validated_data['email'], nickname=self.validated_data['nickname'], password=self.validated_data['password'] ) return user
# views.py class JoinView(APIView): serializer_class = JoinSerializer def post(self, request): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): user = serializer.save(request) token = TokenObtainPairSerializer.get_token(user) refresh_token = str(token) access_token = str(token.access_token) res = Response( { "email": user.email, "nickname": user.nickname, "message": "๊ฐ์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์ด๋ค์ก์ต๋๋ค.", "token": { "access": access_token, "refresh": refresh_token, }, }, status=status.HTTP_200_OK, ) res.set_cookie("access", access_token, httponly=True) res.set_cookie("refresh", refresh_token, httponly=True) return res return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
๋ก๊ทธ์ธ ๊ตฌํ
# serializers.py class LoginSerializer(serializers.Serializer): email = serializers.EmailField(required=True) password = serializers.CharField(write_only=True, required=True) def validate(self, request): email = request.get('email', None) password = request.get('password', None) if User.objects.filter(email=email).exists(): user = User.objects.get(email=email) if not user.check_password(password): raise serializers.ValidationError({"Wrong Password"}) else: raise serializers.ValidationError({"User doesn't exist."}) token = RefreshToken.for_user(user) refresh = str(token) access = str(token.access_token) data = { 'email': user.email, 'refresh': refresh, 'access': access } return data
# views.py class LoginView(APIView): serializer_class = LoginSerializer def post(self, request): serializer = self.serializer_class(data=request.data) if serializer.is_valid(raise_exception=False): email = serializer.validated_data['email'] access = serializer.validated_data['access'] refresh = serializer.validated_data['refresh'] # data = serializer.validated_data res = Response( { "message": "๋ก๊ทธ์ธ๋์์ต๋๋ค.", "email": email, "access": access, "refresh": refresh }, status=status.HTTP_200_OK, ) return res return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
Password Column ๊ธธ์ด ์๋ฌ
ALTER TABLE [TABLE๋ช ] modify [COLUMN๋ช ] VARCHAR(1000);
mysql ๋ช ๋ น์ด๋ก ํด๋น ํ๋ ๊ธธ์ด ๋๋ ค์ ํด๊ฒฐ
๋๋ฌด ์ด๋ ค์ ๋ค..๐ฉ ์ด๋์ ๋ ํ๊ณ ๋์ ๋ค์ ์ด๋ ต์ง ์๊ฒ ์งํ๊ณ ์ฌ์ ๋กญ๊ฒ ํ๋๋ฐ ์ด๋ฆฌํด๋ ์ ๋ฆฌํด๋ ์๋ผ์ ๋ช๋ฒ์ด๋ ๋ค์ํ๊ณ ๊ทธ๋ฌ๋ค. ํํ. ๋ด๊ฐ ํผ์ ๋๋ผ๊ธฐ์๋ ์ง๊ธ ๋ด ์ฝ๋๊ฐ ์๋นํ ๋นํจ์จ์ ์ด๊ณ ๋๋ฌ์ด ๊ฒ ๊ฐ์์ ๋ค์์ ๊ผญ ๋ฆฌํํ ๋ง์ ํ๊ณ ์ถ๋ค. ๊ทธ๋ฆฌ๊ณ ์ ๋ฒ์ viewset์ด๋ url์์ router๋ฅผ ์ฐ๋ ์์ ์ ํ๋ฉด์ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ก๋๋ฐ ์ด๋ฒ ๊ณผ์ ์์๋ ๋ค์ APIView์ as_view()๋ฅผ ์ฌ์ฉํด์ ๋ ๊ฐ์ง ์ฝ๋ ํ์์ด ๊ฐ์ด ์๋๊ฒ ๋ง๋์ง ๋ชจ๋ฅด๊ฒ ๋ค. ์ฐ์ ๋ณด๊ธฐ์ ๊น๋ํ์ง๋ ์์ ๊ฒ ๊ฐ๋ค. ์ผ๋ ๋ฑ๋ ๊ณผ์ ๋ ๐
docker-compose -f docker-compose.yml up --build
ํฐ๋ฏธ๋์์ ์คํํ์ฌ ๋ธ๋ผ์ฐ์ ์์ 127.0.0.1:8000 ์ ์ ํ ์คํธ
์ ์ ์ฑ๊ณต!
์คํํ์ ๋ ๋ชจ๋ ์ํฌํธ ์๋ฌ๊ฐ ๋ง์ด ๋ฐ์ํ๋๋ฐ pip list๋ก requirements.txt์ ์ถ๊ฐ๊ฐ ํ์ํ ๋ด์ฉ๋ค ์ฐพ์์ ์์ ํ์ฌ ํด๊ฒฐํ์๋ค.
docker-compose -f docker-compose.prod.yml down -v
์
๋ ฅํ์ฌ ์ข
๋ฃ
- AWS EC2 ์๋ฒ ๊ตฌ์ถ: ์ฐธ๊ณ ๋งํฌ
- AWS RDS ๊ตฌ์ถ: ์ฐธ๊ณ ๋งํฌ
DATABASE_HOST={RDS db ์ฃผ์}
DATABASE_DB=mysql
DATABASE_NAME={RDS ๊ธฐ๋ณธ database ์ด๋ฆ}
DATABASE_USER={RDS User ์ด๋ฆ}
DATABASE_PASSWORD={RDS master ๋น๋ฐ๋ฒํธ}
DATABASE_PORT=3306
DEBUG=False
DJANGO_ALLOWED_HOSTS={EC2 ์๋ฒ ip ์ฃผ์}
DJANGO_SECRET_KEY={django secret key}
ํ๋ก์ ํธ ์๋จ์ ํ์ผ ์์ฑ ํ ๋ด๊ฐ ๊ตฌ์ถํ ์๋ฒ ๋ด์ฉ ๋ฃ๊ธฐ! โ ๊ธฐ์กด .env ํ์ผ๋ช ์ ๋ฐ๊พธ๊ณ ํด๋น .env.prod๋ฅผ .env๋ก ๋ฐ๊ฟ์ ์ ๋ด์ฉ์ด ์ฐ๊ฒฐ๋๊ฒ ํจ
- ENV_VARS: .env.prod ์ ์ฒด ๋ณต์ฌ ๋ถ์ฌ๋ฃ๊ธฐ
- HOST: ๋ฐฐํฌํ EC2 ์๋ฒ ํผ๋ธ๋ฆญ DNS(IPv4) ์ฃผ์
- KEY: ๋ฐฐํฌํ EC2 ์๋ฒ๋ก ์ ๊ทผ ๊ฐ๋ฅํ ssh key ์ ๋ฌธ (.pem)
deploy.yml
์ branch๋ฅผ master๋ก ์ค์ ํ๊ธฐ ๋๋ฌธ์ master branch์์ pushํ์ ๋ ์๋์ผ๋ก ๋ฐฐํฌ๋๋ค.
postman์์ ๋ฐฐํฌ๋ EC2 DNS ์ฃผ์๋ก ์ ์ํ์ฌ api ํ์ธ
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ณด๋ฉด ์ ์ ์ฅ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ฒซ ๋ฐฐํฌ๋ฅผ ๋๋๋ค!! ๋ด๊ฐ ํ๋ ค๋ ๋ญ ํ๋ ธ๋์ง ํ์ธํ๊ธฐ๊ฐ ์ด๋ ค์์ ์ง๊ธ๊น์ง ํ๋ ๊ณผ์ ์ค์ ์๋์ ๋ ๊ฐ์ฅ ๋ง๋งํ๊ณ ํ๋ค์๋ค.. ์ ๊ฐ ๋ชจ์๋ผ์.. ๋ชจ์๋ผ์ ๊ทธ๋ฝ๋๋ค๐งข ์ด๋ฐ ๋๋ฅผ ๋๊น์ง ๋์์ฃผ์ ๋ฏผ์ค๋๊ป ๊ฐ์ฌ์ ๋ง์ ์ฌ๋ฆฝ๋๋ค ๊ทธ์ ๋น!