class SignUpView(APIView):
def post(self, request):
"""사용자 정보를 받아 회원가입 합니다."""
# 사용자가 이미지 등록을 했을 때
print(request.data) # <QueryDict: {'email': ['7@7.com'], 'password': ['123'], 'fullname': ['7'], 'nickname': ['7'], 'birthday': [''], 'profile_img': [<InMemoryUploadedFile: 족발엔 소맥.jpg (image/jpeg)>]}>
# 사용자가 이미지 등록을 안 했을 때
print(request.data) # <QueryDict: {'email': ['8@8.com'], 'password': ['123'], 'fullname': ['8'], 'nickname': ['8'], 'birthday': [''], 'profile_img': ['undefined'}>
# 'profile_img' : ['undefined'] : javascript에서 파일이 아닌 형식(빈 문자열 등)을 보내주고 있기 때문에 'undefined'라고 보여주고 있습니다. 이는 javascript에서 프로필 이미지 파일을 사용자가 업로드 했을 경우에만 FormData에 넣어주는 조건식을 추가해줘서 이미지 파일이 있을 경우에만 서버에 보내주도록 합니다..
new_user_serializer = UserSerializer(data=request.data) # (역)직렬화 할 데이터는 'Serializer'의 'data'속성에 있습니다.
# 역직렬화 할 데이터인 request.data(즉, 사용자가 보낸 data)를 'UserSerializer 클래스'의 'data'속성에 넣습니다. 이 때, 역직렬화 된 UserSerializer 클래스의 인스턴스가 생성됩니다.
#print(new_user_serializer) # UserSerializer(data={'email': 'postman@postman.com', 'password': '123', 'fullname': '장현웅', 'nickname': '알렉스'}):
# email = EmailField(label='이메일', max_length=255, validators=[<UniqueValidator(queryset=User.objects.all())>])
# password = CharField(label='비밀번호', max_length=255, write_only=True)
# fullname = CharField(label='이름', max_length=50)
# nickname = CharField(label='닉네임', max_length=50, validators=[<UniqueValidator(queryset=User.objects.all())>]) birthday = DateField(allow_null=True, label='생년월일', required=False)
if new_user_serializer.is_valid(): # is_valid() 메서드는 UserSerializer의 data로 전달된 데이터에 대한 유효성을 검사합니다. 이것은 DRF의 공식입니다. DB에 데이터를 저장하기 전, 데이터의 유효성 검사를 필수적으로 수행합니다.
new_user_serializer.save() # '.save()'메서드는 'UserSerializer'를 통해 (역)직렬화된 데이터를 데이터베이스에 저장하는 메서드입니다. 이 시점에서 'UserSerialize'의 create 메서드가 호출됩니다.
return Response({"message":"가입되었습니다."}, status=status.HTTP_201_CREATED) # Response 클래스는 일반적으로 두 가지 필수 인자를 받습니다. (1. HTTP 응답 본문(response body)에 들어갈 데이터, 2. HTTP 상태 코드(Http status code))
else: # HTTP 응답 본문(response body)에 들어갈 데이터 : 요즘은 주로 JSON 형식의 데이터를 반환하기 위해 딕셔너리 형태의 데이터를 전달합니다. DRF의 Response 클래스는 이를 JSON으로 직렬화하여 전달합니다.
return Response({"message":new_user_serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
Serializer : 데이터나 객체를 데이터 저장 장치에 저장하거나, 네트워크를 통해 전송할 수 있는 형식으로 변환하는 도구입니다.
- Serializer's Flow : request(JSON 등의 data) -> Deserializing(역직렬화) -> Django 객체(Save to DB) -> Serialize(직렬화) -> Response(JSON 등의 data)
- Seriallizer for data 검증(Validation) : 주로 역직렬화된 데이터의 무결성과 안전성을 확인하기 위한 과정으로 데이터의 해시값을 계산하거나, 디지털 서명을 검증하거나, 데이터 구조를 확인하는 등의 방법을 사용하여 수행됩니다.
아래 예제와 같이 주로 웹 애플리케이션에서 클라이언트로부터 받은 데이터를 검증하거나, 데이터베이스에서 데이터를 조회한 후 그 결과를 변환하는 데 사용됩니다.
ex. if not email :
retuen Response({"error": "이메일을 입력해주세요."})
if User.objects.filter(id=user_id).exist():
return Response({"error" : "이미 존재하는 사용자 계정입니다."})
class UserSerializer(serializers.ModelSerializer):
""" 회원가입 페이지, 회원 정보 수정 페이지에서 사용자가 보내는 JSON 형태의 데이터를 역직렬화하여 모델 객체 형태의 데이터를 생성하기 위한 Serializer 입니다. """
class Meta: # 'UserSerializer' 클래스를 사용하여 (역)직렬화에 필요한 모델과 필드를 설정합니다.
model = User # settings.py에 정의한 AUTH_USER_MODEL(User 모델)에 대한 (역)직렬화를 수행합니다.
# Case 1 # settings.py에 정의한 AUTH_USER_MODEL(User 모델) 필드 중 클라이언트와 주고 받을 데이터에 해당하는 필드만 (역)직렬화 할 필드에 넣어주겠습니다.
fields = ['email', 'password', 'fullname', 'nickname', 'profile_img', 'birthday']
# Case 2
"""fields = "__all__"""
# Case 3
"""exclude = ["birthday"]""" # (역)직렬화 할 필드에 제외시킬 필드를 설정합니다.
extra_kwargs = { # 'extra_kwargs' : 모델 필드에 대한 추가 설정을 지정하는데 사용됩니다. / ex. extra_kwargs = {'field_name' : {'birthday' : '생년월일'}, 'password' : {'read_only' : True}, 'nickname' : {'required' : False}, }
"password" : {"write_only" : True} # {"password": {"write_only": True}} 설정 : "password" 필드가 쓰기 전용(write-Only)임을 나타냅니다. 클라이언트에서 데이터를 서버로 보낼 때(역직렬화)는 "password" 필드를 제공할 수 있지만, 서버에서 클라이언트에게 응답할 때(직렬화)는 "password" 필드의 값을 숨깁니다.
}
def create(self, validated_data): # User에 경우에는 회원가입, 회원 정보 수정 시에 password를 특수하게 저장하기 때문에 따로 생성, 수정 메서드를 정의해줍니다.
""" 회원가입 시 사용자가 보내는 JSON 형태의 데이터를 모델 객체 형태로 역직렬화하는 메서드입니다. """
password = validated_data.pop("password") # pop 메서드는 특정 키에 해당하는 값을 가져오고 그 값을 딕셔너리에서 삭제하는 역할을 합니다.
# print(validated_data) -> {'email': 'postman@postman.com', 'fullname': '장현웅', 'nickname': '알렉스'}
# 'validated_data'에서 키 'password'의 값만 뽑아내서 변수에 넣어줍니다. 이 작업 후 'validated_data'에는 키 'password'의 값이 삭제된 상태입니다.
user_instance = User.objects.create(**validated_data) # '**validated_data' : 'validated_data'는 딕셔너리를 나타내며, 이 딕셔너리의 내용은 사용자 모델의 필드와 해당 값을 포함합니다. '**'은 파이썬에서 사용되는 연산자로, 키워드 인자를 함수에 전달할 때 사용됩니다.
#user_data = settings.AUTH_USER_MODEL(**validated_data) # '**' 연산자를 사용하여 딕셔너리의 내용을 키워드 인자로 전달하면, 딕셔너리의 각 키가 필드 이름이 되고, 해당 값이 필드에 대한 값을 나타냅니다. 이렇게 하면 사용자 모델의 필드에 대한 값을 설정하고, 새 사용자를 생성할 때 이 값을 사용할 수 있습니다.
# print(user_instance) -> postman@postman.com / 이 User 객체는 model을 정의할 때 __str__메소드로 해당 객체를 email로 표시하겠다고 정의하여 해당 객체의 이름? 설명?을 사용자의 email로 표시합니다.
user_instance.set_password(password) # set_password 메서드는 주어진 비밀번호를 해시 처리하고, 이 해시된 비밀번호를 사용자 모델 객체에 저장합니다.
user_instance.save() # '.save()'를 하면 Serializer는 ORM(객체 관계 매핑)의 일부분을 추상화하여 사용자 모델 객체를 생성하고 이를 데이터베이스에 저장합니다.
return user_instance
이미지 파일도 함께 보내줘야 하기 때문에 FormData로 보내줍니다.
[ signup.html ]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원가입 페이지</title>
<script src="user.js"></script> <!--웹 페이지에 JavaScript 코드를 삽입하기 위해 사용합니다. -->
</head>
<body>
<h1>회원가입 페이지</h1>
<form>
<div>
<label>이메일</label>
<input type="email" id="id_email" placeholder="이메일을 입력해주세요">
</div>
<div>
<label>비밀번호</label>
<input type="password" id="id_password" placeholder="비밀번호를 입력해주세요">
</div>
<div>
<label for="fullname">이름</label>
<input type="fullname" id="id_fullname" placeholder="이름을 입력해주세요">
</div>
<div>
<label for="nickname">닉네임</label>
<input type="nickname" id="id_nickname" placeholder="닉네임을 입력해주세요">
</div>
<div>
<label for="profile_img">프로필 이미지</label>
<input type="file" id="id_profile_img">
</div>
<div>
<label>생년월일</label>
<input type="text" id="id_date_of_birth" size="33" placeholder="(선택) 0000-00-00 생년월일을 입력해주세요">
</div>
<button type="button" onclick="handleSignup()">가입</button> <!-- type이 submit이면 form이 작동하면서 주소창에 localhost:5500/?email=&password= 이렇게 쿼리문이 생깁니다. -->
<!-- 그래서 type을 그냥 button으로 하고 버튼이 눌리면 onclick에 연결된 JavaScript 함수나 코드 블록이 실행되도록 합니다. -->
</form>
</body>
</html>
async function handleSignup(){ // handle~은 JavaScript에서 자주 사용되는 패턴 중 하나로 버튼을 클릭할 때 호출되는 함수를 나타내는 관습적인 이름입니다.
// async는 JavaScript에서 비동기적인 작업을 수행하는 함수를 선언하는 키워드로 코드가 비동기 작업을 수행할 때 다른 작업을 중단하지 않고 계속 진행할 수 있도록 도와줍니다.
const email = document.getElementById("id_email").value // 이 코드는 JavaScript를 사용하여 HTML 문서에서 특정 요소의 값을 가져오는 코드입니다.
const password = document.getElementById("id_password").value // document : 현재 웹 페이지의 DOM(Document Object Model)을 나타냅니다. 이 객체는 HTML 문서의 구조를 표현하고 JavaScript로 조작할 수 있는 방법을 제공합니다.
const fullname = document.getElementById("id_fullname").value // getElementById 함수 : DOM에서 특정 ID를 가진 요소를 선택하는 메서드입니다. 해당 ID를 가진 HTML 요소에 대한 참조를 가져올 수 있습니다.
const nickname = document.getElementById("id_nickname").value // .value : 해당 요소에 입력된 값을 나타냅니다.
const profile_img = document.getElementById("id_profile_img").files[0] // .files 속성 : 파일 입력(input) 요소의 .files 속성은 사용자가 선택한 파일들을 나타내는 FileList 객체를 반환합니다. [0]은 이 FileList에서 첫 번째 파일을 선택하는 것을 의미합니다.
const birthday = document.getElementById("id_birthday").value
//console.log(email, password, fullname, nickname, profile_img, birthday) // 2@2.com 123 2 2 File {name: '족발엔 소맥.jpg', lastModified: 1696330701049, lastModifiedDate: Tue Oct 03 2023 19:58:21 GMT+0900 (한국 표준시), webkitRelativePath: '', size: 689147, …}
const data = new FormData() // FormData 객체를 생성합니다. 이 객체는 폼 데이터를 담을 수 있는 컨테이너입니다. FormData 객체를 사용하면 여러 종류의 데이터를 하나의 객체에 쉽게 추가하고, 이를 서버로 전송할 때 POST 요청을 보낼 수 있습니다.
data.append("email", email) // email이라는 이름의 필드를 생성하고, 그 필드에 email 변수에 저장된 값을 추가합니다.
data.append("password", password)
data.append("fullname", fullname)
data.append("nickname", nickname)
if (profile_img) {
data.append("profile_img", profile_img)
}
data.append("birthday", birthday)
//console.log(data) // FormData {}
// FormData 객체는 일반 JavaScript 객체가 아니므로 반복문을 사용하여 내부 값을 직접 확인하기 어렵습니다.
// 그러나, FormData 객체에서 값을 읽어오려면 FormData의 entries() 메서드를 사용하여 반복 가능한 객체를 얻은 다음 반복문을 사용하여 내부 값을 읽을 수 있습니다.
for (const entry of data.entries()) { // .entries() 메서드 : JavaScript에서 반복 가능한(iterable) 객체에서 키-값 쌍(key-value pair)을 순회(iterate)하는 데 사용되는 메서드입니다. 주로 배열, 맵(Map), 셋(Set), 객체(Object), FormData와 같은 데이터 구조에서 사용되며, 이 경우에는 FormData 안에 있는 키-값 쌍을 가져와 [key, value] 형식의 배열로 반환합니다.
const [key, value] = entry;
console.log(entry) // ['email', '6@6.com'] ['password', '123'] ['fullname', '6'] ['nickname', '6'] ['profile_img', File] ['birthday', '']
console.log(`${key}: ${value}`); // email: 4@4.com user.js:37 password: 123 user.js:37 fullname: 4 user.js:37 nickname: 4 user.js:37 profile_img: [object File] user.js:37 birthday:
} // `${}` : JavaScript의 문자열 템플릿 리터럴 문법을 사용한 문자열 표현입니다. 변수나 표현식을 문자열에 삽입할 수 있습니다. `${key}: ${value}`는 이 두 변수의 값을 문자열 내에 삽입하여 출력하는데 사용됩니다.
const response = await fetch('http://127.0.0.1:8000/user/signup/', { // await : JavaScript의 async 함수 내부에서 사용하는 비동기 처리에 사용되는 키워드입니다.
// await 키워드는 코드 실행을 일시 중단하며, 기다린 후에 다음 작업을 시작합니다. await 키워드를 사용하는 부분에서만 멈추고, 다른 부분의 코드는 계속 실행됩니다.
// await fetch 부분에서 HTTP POST 요청을 보냅니다. 이때, Javascript 엔진은 응답이 올 때까지 기다리지 않고 다음 코드를 실행합니다.
// 다만 해당 코드 줄에 한해서 요청 처리를 기다리고 응답이 오면 반환합니다. 그리고 응답을 기다리는 동안 이미 진행중이던 코드에 이어서 실행됩니다.
headers : { // fetch 함수의 두 번째 인자로 전달되는 객체는 HTTP 요청 옵션을 정의합니다.
//'Content-type' : 'application/json', // HTTP 요청 헤더의 이 설정은 요청의 본문(body) 데이터가 JSON 형식으로 전송된다는 것을 서버에 알려줍니다.
// 'Content-type' : 'multipart/form-data', // 일반 딕셔너리형 문자열을 서버로 보내줄 때는 'Content-type'을 json이라고 명시해줘야 합니다. 하지만 FormData를 생성해서 보내주는 경우 브라우저에서는 자동으로 Content-Type을 'multipart/form-data'로 설정합니다.
},
method : 'POST',
body : data
// JSON.stringify({ // fetch 함수로 보내는 HTTP 요청의 본문(body) 데이터를 정의하는 부분입니다.
// "email":email, // JSON.stringify() 함수 : fetch 함수의 body 속성에는 데이터를 객체 형식으로 넣는 것이 아니라 문자열로 넣어야 하기 때문에 JSON.stringify() 함수를 사용하여 데이터 객체를 JSON 문자열로 변환한 후에 body에 넣어줘야 합니다.
// "password":password, // POSTMAN에서 Body의 옵션 중 문자열 데이터 객체(raw)를 JSON 형식으로 선택해서 HTTP 요청을 보내주는 과정과 같습니다.
// "fullname":fullname,
// "nickname":nickname,
// "birthday" : birthday
// })
})
// 현재 상태로 웹 페이지에서 회원가입을 진행하면 아래의 오류가 납니다.
// console.log(response)
// Access to fetch at 'http://127.0.0.1:8000/user/signup/' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy:
// Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
// 웹 페이지가 'http://127.0.0.1:8000' 서버에서 리소스를 요청하려고 하지만, 원래 출처(origin)인 'http://127.0.0.1:5500'에서는 CORS 정책을 위반하고 있다는 것을 나타냅니다.
// 서버에서 올바른 CORS 헤더를 포함하지 않아, 브라우저는 이 요청을 미리 확인(preflight)하고 허용하지 않았습니다. 서버에서 'Access-Control-Allow-Origin' 헤더를 포함하도록 설정되어 있지 않아서 나는 오류입니다.
// -> 이 오류 메시지는 CORS(Cross-Origin Resource Sharing) 정책으로 인해 발생한 것입니다. 브라우저는 보안상의 이유로 웹 페이지가 다른 도메인에서 리소스를 요청할 때 CORS 정책을 적용하며, 서버에서 허용하지 않는 경우 요청을 차단합니다.
// => CORS 헤더를 추가하면 다른 도메인에서 리소스에 액세스할 수 있습니다. CORS에 필요한 서버 헤더를 처리하기 위한 Django App 'django-cors-headers'를 설치하겠습니다. (참고 : https://pypi.org/project/django-cors-headers/)
CORS는 웹 페이지가 다른 도메인에서 리소스를 요청할 때 보안을 유지하기 위한 웹 브라우저의 정책입니다. 서버에서 어떤 웹 사이트가 요청을 수락하거나 거부할 수 있는지를 설정하여 제어합니다.
[ Terminal ]
pip install django-cors-headers
[ settings.py ]
INSTALLED_APPS = [
...,
"corsheaders",
...,
]
MIDDLEWARE = [
...,
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
...,
]
웹 애플리케이션에서 Cross-Origin Resource Sharing (CORS)을 구성하는 데 사용됩니다.
[ CORS_ALLOW_ALL_ORIGINS = bool]
- true로 설정하면 모든 도메인에서의 요청을 허용합니다.
- 개발 단계에서 혹은 디버깅에만 사용합니다.
[ CORS_ALLOWED_ORIGINS ]
- CORS_ALLOWED_ORIGINS = [
"https://example.com",
"https://sub.example.com",
"http://localhost:8080",
"http://127.0.0.1:9000",
]
- CORS 접근을 허용할 도메인의 목록을 지정합니다.
[ CORS_ALLOWED_ORIGIN_REGEXES ]
- CORS_ALLOWED_ORIGIN_REGEXES = [
r"^https://\w+\.example\.com$",
]
- 정규 표현식을 사용하여 허용된 도메인을 지정합니다.
- 위의 예제를 설명하면, 모든 https://로 시작하고 .example.com으로 끝나는 도메인을 허용합니다.
🥚🐣🐤🐥🐓🐔