안녕하세요. 개발자 최영철입니다.
이번 포스트에서는 홈페이지에서 프로젝트 목록을 불러오고, 삭제, 추가할 수 있게 만드는 기능을 구현하기 위해 백엔드를 구현하는 이야기를 하려고 합니다.
목차
1. MySQL Escape Sequence
이전 포스트에서 작성한 방명록을 직접 테스트 하던 도중, 한 줄이 아닌 여러 줄로 작성해서 내용을 보냈는데 개행 문자인 \n이 공백으로 교체되는 현상을 목격했다.
보낸 내용
1\n2\n3\n4\n5 (줄바꿈이 자동으로 \n으로 들어감)
실제로 MySQL에 저장된 내용
1 2 3 4 5
문제를 해결하기 위해 먼저 Django에서 데이터베이스에 보내는 내용을 Serializer의 create 함수에서 확인해봤다.
def GuestBookManageSerialzier(serializers.ModelSerializer):
def create(self, validated_data):
print(validated_data)
return super().create(validated_data)
#Output
#{..., "content": "테스트\n입니다.", ...}
Output을 확인 해본 결과 \n이 제대로 남아있는 것을 알았다. 그렇다면 MySQL에서 저장하는 과정에서 이것이 변한다는 것인데 이것에 대한 내용을 조사해봤다.
검색 결과 이러한 현상이 일어나는 이유는 MySQL이 \n를 Escape Sequence로 인식하고 이를 실제 줄바꿈으로 치환하고 저장하기 때문이었다. MySQL은 \를 escape character로 인식하고 뒤에 나오는 글자에 따라 치환을 한다. 아래 escapce sequence 중 \n는 newline으로 치환되는 것을 알 수 있다. 이렇게 여러 줄로 치환된 텍스트는 한 줄로 바뀔 때 한 칸 띄우고 다음 줄 내용이 들어오는 식으로 바뀌므로 "1\n2\n3\n4\n5"가 "1 2 3 4 5"로 바뀐 것이다.
\0 An ASCII NUL (0x00) character.
\' A single quote (“'”) character.
\" A double quote (“"”) character.
\b A backspace character.
\n A newline (linefeed) character.
\r A carriage return character.
\t A tab character.
\Z ASCII 26 (Control-Z). See note following the table.
\\ A backslash (“\”) character.
\% A “%” character. See note following the table.
\_ A “_” character. See note following the table.
이것을 해결하기 위해 Django에서 \n를 \\n으로 바꾸는 작업을 수행하도록 serializer에 다음 코드를 넣었다.
validated_data['content'] = '\\n'.join(validated_data['content'].split('\n'))
이렇게 성공적으로 DB에 \n이 저장되는 것을 확인할 수 있었다.
2. ProjectSerializer 구현
위에서 \n 글자가 저장되지 않는 문제를 해결 했으니 이제 Serializer의 구현을 시작했다.
Project API 경우, 일반 유저는 GET만 가능하고 데이터의 추가와 삭제는 어드민 유저만 가능하게 만들 예정이었다. 이번의 경우 GuestBook에 존재하지 않는 type 필드나 password 등 민감한 필드를 반환하지 않도록 하는 작업이 없었기 때문에 Project는 모든 필드를 사용하는 하나의 Serializer만 구현했다.
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__'
def create(self, validated_data):
validated_data['content'] = '\\n'.join(validated_data['content'].split('\n'))
return Project.objects.create(**validated_data)
이번 Serializer를 구현할 때 위에서 배운 내용을 바로 써먹을 수 있었다.
3. Project APIView
class ProjectBoardView(APIView):
def get(self, request):
try:
objs = Project.objects.all()
serializer = ProjectSerializer(objs, many=True)
data = serializer.data
return Response(data)
except:
return Response(status=404)
def post(self, request):
try:
files = request.FILES
data_dict = dict(request.POST)
for key, value in data_dict.items():
data_dict[key] = value[0]
if 'img' in files:
data_dict.update({'img': files['img']})
serializer = ProjectSerializer(data=data_dict)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response("saved")
raise
except Exception as e:
return Response(serializer.errors, status=400)
def delete(self, request):
try:
data = request.data
userid = TokenManager.decode(header=request.headers)
user = User.objects.get(
groups__name = 'admin',
id = userid,
)
obj = Project.objects.get(id=data['id'])
obj.delete()
return Response(status=200)
except Exception as e:
return Response(status=400)
Project API의 구현 이전에 GuestBook API와 거의 비슷하므로 POST에서 왜 request.FILES와 request.POST를 사용하는지, DELETE의 groups__name은 무엇인지 알아보자.
4. Content-Type, request.FILES & POST
post 함수에서 body 데이터를 request.FILES와 POST로 받는 이유는 Body 데이터에 파일이 담긴 요청 헤더의 Content-type은 multipart/form-data 이기 때문입니다.
주로 사용되는 Content-type은 다음과 같고, 각 타입마다 특징을 가지고 있습니다.
application/x-www-form-urlencoded | /path?key1=value1&key2=value2와 같이 보낼 데이터를 URL에 query parameter로 보내는 방식이다. |
application/json | 보낼 데이터를 JSON 타입으로 보내는 방식이다. |
multipart/form-data | 단순히 json로 문자열을 보내는 것이 아니라 이와 같이 파일도 보내기 때문에 요청을 받는 서버에서 이를 처리할 수 있도록 고안된 type이다. |
Project 모델의 경우 img 필드를 만들어놨기 때문에 클라이언트에게서 제목, 내용뿐만 아니라 이미지 파일도 받기 위해서 프론트엔드에서 백엔드에 Form Data로 보내도록 할 것이다.
Django Rest Framework에서 Form Data로 헤더를 받은 request의 데이터를 단순히 request.DATA로 접근하게 된다면
<QueryDict: {'title': ['제목'], 'content': ['내용'], 'img': [<InMemoryUploadedFile: test.png (image/png)>]}>
위와 같이 각 key에 value가 리스트에 담긴 채 반환되게 된다. 만약 이를 사용해 처리한다면 리스트의 내용을 뽑아내는 불필요한 과정이 추가되고 img 키에 담긴 것이 파일인지 확인하는 과정도 추가될 것이다. 이를 방지하기 위해 request.FILES와 POST를 이용해서 데이터를 받는다.
5. Django ManyToManyField
위의 사진은 DB ER다이어그램으로 우리가 구현한 적이 없는 UserGroups와 Groups도 나타나있다. Groups는 Django 자체에서 그룹을 관리하기 위해 생성하는 테이블이라고 하면 Foreign Key 밖에 없는 UserGroups는 갑자기 어디서 나타난 테이블일까? 이를 이해하려면 Django에서 제공하는 다대다 관계를 구현하기 위한 ManyToManyField에 대해 알아야한다.
유저 테이블과 영화 테이블이 있다고 하자. 그리고 유저가 즐겨찾는 영화를 저장하고 불러올 수 있는 기능을 구현하려고 한다. 유저는 다양한 영화를 즐겨찾기할 수 있고, 영화는 여러 유저에게 즐겨찾기될 수 있다. 이를 다대다 관계라고 한다. 영화 테이블에 단순히 유저를 ForeignKey 필드로 추가해서 즐겨찾기 기능을 구현한다고하자. 그렇다면 여러 유저가 A 영화를 추가할 때마다 같은 이름의 영화가 유저 값만 다른 채 계속 생성될 것이다. 이것은 공간적으로 비효율적이고 쿼리 성능에도 영향을 미친다.
이러한 문제는 유저와 영화를 Foreign Key로 받는 중간 테이블이 있으면 해결할 수 있는데 여기에서 ManyToManyField가 등장한다. 기본적으로 특정 모델에 ManyToMany Field를 선언하고 다대다 관계를 가질 다른 모델을 정해주면 Django는 자동으로 모델을 생성해준다. 방금 전에 들었던 유저와 영화의 예시를 다시 사용하면
#중간 테이블을 자동으로 생성할 경우
class Movie(...):
id = models.BigAutoField(primary_key=True)
class User(...):
id = models.BigAutoField(primary_key=True)
likes = models.ManyToManyField(Movie)
#중간 테이블을 직접 생성할 경우
class Movie(...):
id = models.BigAutoField(primary_key=True)
class User(...):
id = models.BigAutoField(primary_key=True)
likes = models.ManyToManyField(Movie, through=UserMovie)
class UserMovie(...):
user_id = models.ForeignKey(User, on_delete=models.CASCADE)
movie_id = models.ForeignKey(Movie, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
다음과 같이 모델을 설정할 수 있고 Django는 through를 통해 사용할 중간 테이블을 정해주지 않는다면 자동으로 중간 테이블을 생성해준다. 이렇게 생성된 테이블은 원하는 필드를 넣으면서 확장하는 것이 어렵다. 따라서 두번째 예제처럼 직접 중간 테이블을 구현하고 through를 통해 연결하면 생성된 날짜, 이 영화를 즐겨찾기한 이유 등 원하는 필드를 넣을 수 있다.
이렇게 생성된 ManyToManyField는 기본적으로 참조하는 모델을 중간테이블에 추가하는 add, 필터링해주는 filter를 가지고 있고 참조 당하는 모델에서는 {참조하는 모델의 이름}_set으로 역참조가 가능하다. 아니면 ManyToManyField를 선언할 때 related_name를 사용하면 원하는 이름으로 역참조가 가능하다.
이렇게 ManyToManyField에 대한 설명은 마무리하고 다시 원래 설명하려던 groups__name으로 돌아가면
user = User.objects.get(
groups__name = 'admin',
id = userid,
)
User의 기본 모델에는 groups=ManyToManyField(Groups)라는 필드가 있고, __name으로 유저가 갖고 있는 그룹의 이름을 참조해서 필터링을 할 수 있기 때문에 사용했습니다.
6. 마무리
이렇게 GuestBook과 비슷하지만, 이미지 파일과 ManyToManyField의 사용으로 인해 달라진 점을 새로운 정보와 함께 이야기할 수 있었습니다. 다음 포스트에서는 GuestSender 컴포넌트를 구현하면서 사용한 기술과 지식, react-slick을 이용해 프론트엔드에서의 프로젝트 기능 구현에 대해 이야기 하겠습니다.
감사합니다!
'FE & BE' 카테고리의 다른 글
9. 원 페이지 스크롤 구현 [MyHomepage: 개인 홈페이지 만들기] (0) | 2022.11.20 |
---|---|
8. 프로젝트 기능 프론트엔드 구현 [MyHomepage: 개인홈페이지 만들기] (0) | 2022.11.19 |
6. 방명록 프론트엔드 구현 [MyHomepage: 개인 홈페이지 만들기] (0) | 2022.11.17 |
5. React Hook과 방명록 백엔드 구현 [MyHomepage: 개인 홈페이지 만들기] (0) | 2022.11.16 |
4. React 모달창 구현 + Styled Components [MyHomepage: 개인 홈페이지 만들기] (0) | 2022.11.15 |
댓글