서버개발자가 되는법 - #5 To-Do 앱 만들기 [1], 기획과 화면을 보고 만들어보기

     

목차 - 2020/09/29 - [Study/서버] - 서버개발자가 되는법 - 목차

 

git - tkdlek11112/server_dev at 서버개발자_5 (github.com)

git(화면) - tkdlek11112/todo-list (github.com)

 

유튜브 - 바쁘신분들은 유튜브 스킵해도 됩니다~

 

 

들어가기 전에

 

지난 강의인 서버 개발자가 되는 법 4편에서 장고(Django)를 이용해서 투두앱(To-Do list)을 만들기 시작했습니다. 잠깐 지난 시간에 만들어진 화면을 볼까요?

 

 

.......

 

네 아주 허접합니다 ㅋㅋㅋㅋㅋㅋㅋ

 

지난번 강의를 하고 나서 아주 깊은 고민에 빠졌는데요... 아래 두 가지에 대해서 많은 고민을 했습니다.

 

1. 강의가 너무 길지 않나?
2. 화면이 너무 구리지 않나?
3. 클라이언트와 협업을 좀 더 실제상황가 같이 만들 수 없을까?

 

위와 같은 고민을 하다가, 기획과 클라이언트 화면이 준비되어 있을 때 서버 개발자가 어떻게 하는지 설명하기 위해, "기획가 클라이언트 화면"을 준비해보기로 했습니다. 그러기 위해서 프런트 개발인 'React'를 공부했는데요 ㅋㅋ 오래 공부한 건 아니고 서버 개발에 도움이 될 정도의 화면 정도는 개발할 수 있도록 공부했습니다.

 

이제 이번 포스팅에서는 아래와 같은 To-Do 앱을 단계별로 만들어 볼 겁니다.

 

요런 화면입니다.

 

상단에 '예제'와 '실습'버튼이 있는데요, 예제를 누르면 화면이 어떻게 동작하는지 볼 수 있고, 실습은 실제 여러분이 만든 장고 서버로 api를 날려서 데이터를 가져와서 테스트해볼 수 있는 구조로 되어있습니다. 

 

이 화면은 제가 만든 프론트화면을 docker로 만들어서 여러분 컴퓨터에서 docker로 실행시키면 127.0.0.1:3001 주소를 이용해 띄울 수 있습니다. 이 프론트화면에 실습화면에서는 실제로 127.0.0.1:8000 (여러분이 pycharm으로 띄운 장고)로 api를 호출하기 때문에 실제로 여러분이 만든 서버가 잘 동작하는지 알 수 있습니다.

 

일단 프론트 화면을 다운 받아 볼까요?

 

 

To-Do 프론트 화면 다운받기

 

기존 강의 중에 Docker강의를 보신 분이라면 컴퓨터에 Docker가 설치되어 있을 겁니다. Docker가 설치되어있다는 가정하에 프론트를 다운받는 깃헙(github)에 들어가서 소스를 받아주세요.

 

프론트 깃헙 - tkdlek11112/todo-list (github.com)

 

 

코드 다운로드를 통해 받자

 

깃헙에서 받으셔도 되고, 소스트리를 사용해서 받으셔도 되고, 터미널에서 클론 받으셔도 됩니다. 

 

소스를 받으시면 커맨드 창을 여시고 docker-compose.yml이 있는 경로로 이동해서 실행 명령어를 입력하면 실행됩니다.

 

먼저 build 명령어를 입력하시면 이미지를 빌드합니다.

 

docker-compose build

 

docker-docmpose build

 

빌드할 때 빨간 글씨로 warning이 뜨는데 무시하셔도 됩니다. 제가 프론트를 만들 때 패키지를 옛날 버전을 사용해서 나오는 경고입니다 ㅎㅎ;;

 

설치가 정상적으로 되면 up 명령어를 통해 도커를 실행시키면 됩니다.

 

docker-compose up -d

docker-compose up -d

 

여기서 -d 옵션은 데몬 형태로 기동 하는 것이라 옵션을 주지 않으면 터미널 창으로 다시 빠져나오지 않습니다 ㅎㅎ

 

도커의 상태를 보려면 docker-compose ps를 입력하면 됩니다.

 

docker-compose ps

 

docker-compose ps

 

 

여러분이 윈도우시라면 docker desktop 화면에서 실행 중인 도커를 확인할 수 있습니다.

 

실행중인 컨테이너

 

docker-compose ps를 눌렀을 때 Ports가 0.0.0.0:3001 -> 3000/tcp로 되어있는 것을 확인할 수 있습니다. 제가 도커 안에 리액트를 3000번 포트로 띄워노았고, 도커 밖에서 3001 포트로 접속하면 도커 안에 3000번 포트로 이어지게 됩니다.

 

즉 127.0.0.1:3001로 접속하면 도커 안에 리액트로 접속할 수 있습니다. 도커가 실행 중일 때 브라우저에서 127.0.0.1:3001을 실행시켜봅시다. 아 그리고 크롬 브라우저 사용을 추천드립니다 ^^

 

 

 

1번은 로그인 화면이고 2번은 To-Do 목록 조회 화면입니다. 현재는 1번과 2번만 구현되어있는 상태인데, 나중에 받으시는 분은 그 뒤에 번호들도 아마 구현이 되어있을 겁니다. (아마도,,,)

 

예제 1번과 2번은 우리의 장고 서버가 실행되지 않아도 동작합니다. 어떤 식으로 동작되는지 제가 프론트에서 구현해놨기 때문에 서비스가 어떻게 실행되는지 확인할 수 있습니다.

 

하단 영역에는 화면에 대해 구현해야 하는 기능과 API 목록이 나와있습니다.

 

 

노란색 박스가 화면에서 구현되어야 하는 기능들, 녹색은 API목록입니다. 실습 버튼을 누르면 아마 기능이 동작을 하지 않을 텐데 이 녹색 박스 안에 있는 API들을 장고로 만들어 주시면 동작하게 됩니다.

 

 

 

회원가입 만들어보기

 

자 그럼 예제 1을 누릅니다.

 

예제 1 화면

 

예제이기 때문에 서버가 없어도 동작하는데요, 대충 어떤 동작을 하는지 하나하나 눌러봅니다. 기능을 읽어보면 아래와 같습니다.

 

- 아이디와 비밀번호를 입력하고 회원가입 버튼을 누르면 회원가입 API를 호출한다.
- 기존에 있는 아이디일 경우 ERROR 응답
- 로그인/회원가입 시 아이디가 빈값일경우 ERROR 응답
- 비밀번호가 틀릴경우 ERROR 응답
- 아이디/비밀번호 일치할 경우 로그인 성공 메시지 + 사용자 ID 응답

 

하나씩 눌러보자

 

하나씩 눌러보면 어떻게 작동되는지 감이 오실 겁니다. 

 

그럼 실습 1을 눌러보세요. 실습 1 화면에서는 로그인과 회원가입을 눌러도 반응을 안 합니다. 네 여기서부터는 Django 서버가 필요한데요, 일단 아래 API 명세를 한번 읽어봅니다.

설명 한번 읽어보자

 

일단 이 화면에서는 두 개의 API가 필요합니다. [* 로그인 API]와 [* 회원가입 API]입니다. API 명세서를 보면 첫 번째에 URL이라고 적혀있는데, 이 URL로 호출하면 된다는 뜻입니다. pycharm에서 장고를 로컬로 실행했을 경우 8000번 포트가 디폴트이기 때문에 8000번으로 지정했고요 로컬 호스트로 호출하기 때문에 서버에 장고를 올렸을 경우에는 이 화면에서 호출을 못합니다. ㅋㅋ 넵 이화면은 로컬에 장고로만 호출할 수 있습니다. 결국 localhost:3001 -> localhost:8000으로 호출이죠.

 

그 아래 입력 필드와 출력 필드는 입력과 출력의 Json파일 포맷입니다. 현업에서는 흔히 I/O(아이오)라고 부립니다. I/O로 부르는 이유는 Input / Output 이기 때문입니다. 이건 회사마다 표기하는 형식이 다른데, 엑셀에 표로 정리하는 팀도 있고, 아예 Json으로 표기하는 팀도 있습니다. 저는 그냥 텍스트로 표기했습니다. 제일 왼쪽에 있는 단어가 필드 이름이고, 그 옆에 괄호 안에 있는 건 필드 타입입니다. 그리고 : 오른쪽에 있는건 설명입니다.

 

로그인 API는 입력이 string 타입인 두 개의 필드 'user_id'와 'user_pw'입니다. 즉 입력이 json으로 아래와 같이 들어간다는 거죠.

{
    "user_id": "test_user_id",
    "user_pw": "test_pw"
}

마찬가지로 출력은 다음과 같이 나옵니다.

{
    "msg":"회원가입 성공했습니다.",
    "user_id":"test_user_id"
}

{
    "msg":"회원가입 실패",
}

 

괄호 안에 option이라고 적은 것은 있을 수도 있고 없을 수도 있는 필드를 가리킵니다. 말 그대로 옵션인데요, option이기 때문에 서버에서든 클라에서든 이 필드가 없을 때 로직이 잘 처리되도록 예외처리를 해주셔야 합니다.

 

반대로 꼭 있어야 하는 필드는 mandatory라고 표시합니다. 여기서는 option이라고 표기 안 했으면 mandatory 필드인 거예요 ㅎㅎ

 

자 그럼 로그인과 회원가입이 있으니 회원가입부터 만들어야겠죠?

http://localhost:8000/login/regist_user를 만들어봅시다.

(사실 이전 강의에서 거의 만들었지만 다시 한번 복습 겸 ^^)

 

일단 프로젝트에 login앱 폴더에서 views.py에 회원가입 기능을 만듭니다. 입력으로 user_id와 user_pw가 올라온다고 했으니 두 개를 받아서 출력 먼저 해봅시다.

 

아, 여기서 잠시. 제가 항상 말하는 게 있는데 클라 개발자를 믿지 말라곸ㅋㅋㅋㅋㅋ

api 명세서에는 user_id와 user_pw가 있지만 실제로 클라이언트가 이렇게 올리는지 모르잖아요? 의심해봐야 됩니다.

그럼 실제로 클라이언트에서 값이 어떻게 올라오는지 서버에서 어떻게 확인할까요?

 

.

.

생각보다 간단합니다. request의 data를 print로 찍어보면 됩니다.

 

일단 views.py에 회원가입 class를 만들어 볼게요.

 

# ~/login/views.py

class RegistUser(APIView):
    def post(self, request):
        print(request.data)

        return Response(status=200)
# ~/login/urls.py
from django.conf.urls import url
from . import views


urlpatterns = [
    url('regist_user', views.RegistUser.as_view(), name='regist_user'),
    url('app_login', views.AppLogin.as_view(), name='app_login'),
]

 

자 이렇게 만들었습니다. (기존에 만든 건 지웠습니다 ㅎㅎ)

이제 ~/login/regist_user를 호출하면 RegistUser가 실행되면서 request의 data영역을 콘솔에 찍을 겁니다.

 

pycharm에서 run을 누르고, 로그인 화면에서 id와 pw에 아무거나 입력하고 회원가입 버튼을 눌러봅시다.

 

회원가입 클릭
pycharm 콘솔창에서 확인

 

오 잘 보이네요 ㅋㅋㅋ

 

여기서 우리는 두 가지를 확인할 수 있습니다.

 

1. docker로 띄운 클라이언트 화면에서 장고로 api 콜이 잘 들어온다.

2. 클라이언트에서 회원가입을 누를 때 user_id와 user_pw가 잘 들어온다.

 

 

이제 user_id와 user_pw로 회원가입을 시키면 될 것 같은데요, API 명세에 출력에 보니까 msg와 user_id를 달라고 합니다. 그럼 회원가입을 시키고, 성공했다는 메시지를 msg에 넣고, 만들어진 user_id(클라에서 올라온 user_id)를 반환해주면 되겠죠?

 

참고로 테스트에 사용하는 회원정보 모델은 아래와 같이 생겼습니다.

# ~/login/models.py
from django.db import models


class LoginUser(models.Model):
    user_id = models.CharField(max_length=20, unique=True, null=False, default=False)
    user_pw = models.CharField(max_length=255, null=False, default=False)

    birth_day = models.DateField(verbose_name="생년월일", null=True)
    gender = models.CharField(verbose_name="성별", max_length=6, null=False, default='male')
    email = models.CharField(verbose_name="이메일 주소", max_length=255, null=False, default=False)
    name = models.CharField(verbose_name="이름", max_length=20, null=False, default=False)
    age = models.IntegerField(verbose_name="나이", default=20)

    class Meta:
        db_table = 'login_user'
        verbose_name = '로그인 테스트 테이블'

 

user_id와 user_pw만 들어오니 아래 birth_day나 gender는 공백으로 처리해도 상관없을 것 같네요. 일단 들어온 값으로만 모델에 채워서 만들어보겠습니다.

 

# ~/login/views.py
class RegistUser(APIView):
    def post(self, request):

        user = LoginUser.objects.create(user_id=request.data['user_id'], user_pw=request.data['user_pw'])

        return Response(status=200, data=dict(msg="회원가입 성공", user_id=user.user_id))

 

자 이제 회원가입이 되는지 해볼까요?

회원가입 성공

서버에서 msg에 넣어준 메시지가 상자 아래 뜨는 것을 확인할 수 있습니다.

DB에도 잘 들어가는지 봐볼까요?

 

DB에 잘 들어갔습니다!

 

참고로 이 포스팅에서는 sqlite를 쓰고 있는데요, 다른 db를 사용하실 분은 settings.py에서 바꿔주시면 됩니다. 

 

# ~/server_dev/settings.py
# mysql 사용시
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.mysql',
#         'HOST': 'choaws.duckdns.org',
#         'NAME': 'server_dev',
#         'USER': 'root',
#         'PASSWORD': 'admin123!',
#         'PORT': '3306',
#         'OPTIONS': {'charset': 'utf8mb4'},
#     }
# }

# sqlite 사용시
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'db.sqlite3',
    }
}

 

sqlite를 사용할 경우 pycharm에 있는 database tool을 이용해 접속할 수 있습니다.

database tool -> data source -> sqlite
file 경로를 프로젝트 경로에 있는 sqlite3을 선택해주면 됩니다.

 

이게 파이참 프로에서 제공하는 기능인지 아니면 커뮤니티 버전에도 있는 건지 잘 모르겠네요 ㅎㅎㅎ 프로버전을 사용합시다. ( 대학생은 학교 메일만 있으면 공짜입니다~ )

 

 

 

 

자 다시 본론으로 돌아가서, 회원가입을 만들었지만 몇 가지 예외처리를 해주어야 합니다. 만약 아이디와 패스워드를 입력하지 않고 회원가입을 누르면 어떻게 될까요?

 

???? 선생님?
음? ㅋㅋㅋ

네,, 이렇게 공백으로 들어와 버립니다 ㅋㅋㅋㅋ

 

개발을 하다 보면 이런 식으로 '예외처리'를 해야 하는 부분들이 많은데요 이런 예외처리를 클라이언트에서 할지, 서버에서 할지도 큰 의사결정 포인트입니다. 물론 가장 좋은 방법은 양쪽 모두 하는 방법입니다. 혹시 클라에서 예외처리 안될 수 있으니 서버에서도 만들고, 혹시 서버에서 안될 수 도 있으니 클라에서도 하는 완벽한 방법이지요. 서로를 못 믿는 클라 vs 서버 개발자들에게 아주 좋은 방법입니다 ㅋㅋㅋ

 

클라는 이미 만들어진 화면을 사용 중이니 수정은 안 되겠고, 서버에서 예외처리를 만들어 봅시다.

 

# ~/login/views.py
class RegistUser(APIView):
    def post(self, request):

        user_id = request.data['user_id']
        if user_id == '' or None:
            Response(status=200, data=dict(msg="아이디는 공백이 될 수 없습니다!!"))

        user_pw = request.data['user_pw']
        if user_pw == '' or None:
            Response(status=200, data=dict(msg="비밀번호는 공백이 될 수 없습니다!!"))
            
        user = LoginUser.objects.create(user_id=user_id, user_pw=user_pw)

        return Response(status=200, data=dict(msg="회원가입 성공", user_id=user.user_id))

 

아이디 미입력시
비밀번호 미입력시

 

자 완료입니다.

 

그럼 이제 기존에 있는 ID로 회원가입을 할 때 이미 있는 ID라고 알려줘야 합니다. 그러기 위해서는 LoginUser에 새로운 user를 만들기 전에 기존에 user가 있는지 체크해야 합니다. 

 

class RegistUser(APIView):
    def post(self, request):

        user_id = request.data['user_id']
        # 아이디 공백 체크
        if user_id == '' or None:
            return Response(status=200, data=dict(msg="아이디는 공백이 될 수 없습니다!!"))

        user_pw = request.data['user_pw']
        # 패스워드 공백 체크
        if user_pw == '' or None:
            return Response(status=200, data=dict(msg="비밀번호는 공백이 될 수 없습니다!!"))

        # 이미 존재하는 아이디인지 체크
        if LoginUser.objects.filter(user_id=user_id).exists():
            return Response(status=200, data=dict(msg="이미 존재하는 아이디 입니다."))

        user = LoginUser.objects.create(user_id=user_id, user_pw=user_pw)

        return Response(status=200, data=dict(msg="회원가입 성공", user_id=user.user_id))

 

있는지 체크할 때는 exists()를 사용하는 게 효과적입니다. 있으면 True 없으면 False를 반환합니다.

 

이미 있는 아이디를 입력하면 이미 존재하는 아이디 입니다 리턴

 

네 이제 중복체크까지 완료했는데요, 아까 테이블을 볼 때 뭔가 위화감이 들지 않았나요?

 

비밀번호가 그대로!?!?

 

네 다시 보시면 비밀번호가 사용자가 입력한 값 그대로 들어간 걸 볼 수 있습니다. 실제 운영하는 서비스에서 비밀번호가 저렇게 들어가 있으면 벌금 냅니다 ㅋㅋㅋ

 

이전 강의에서 했던 방식으로 암호화를 해볼까요?

 

# ~/login/views.py
from django.contrib.auth.hashers import make_password

class RegistUser(APIView):
    def post(self, request):

        user_id = request.data['user_id']
        # 아이디 공백 체크
        if user_id == '' or None:
            return Response(status=200, data=dict(msg="아이디는 공백이 될 수 없습니다!!"))

        user_pw = request.data['user_pw']
        # 패스워드 공백 체크
        if user_pw == '' or None:
            return Response(status=200, data=dict(msg="비밀번호는 공백이 될 수 없습니다!!"))

        # 이미 존재하는 아이디인지 체크
        if LoginUser.objects.filter(user_id=user_id).exists():
            return Response(status=200, data=dict(msg="이미 존재하는 아이디 입니다."))

        # 암호화 해서 집어넣기
        user = LoginUser.objects.create(user_id=user_id, user_pw=make_password(user_pw))

        return Response(status=200, data=dict(msg="회원가입 성공", user_id=user.user_id))

다른 아이디로 회원가입
암호화 되었다!

 

django.contrib.auth.hashers 에서 제공하는 make_password를 가져와서 LoginUser에 넣을 때 패스워드를 make_password로 감싸줍니다. 그럼 자동으로 암호화되어서 DB에 저장됩니다.

 

 

 

 

로그인 만들어보기

 

회원가입과 마찬가지로 로그인도 같은 방법으로 일단 들어오는 값을 찍어봅니다.

 

# ~/login/views.py

class AppLogin(APIView):
    def post(self, request):
        print(request.data)

        return Response(status=200)
# ~/login/urls.py
from django.conf.urls import url
from . import views


urlpatterns = [
    url('regist_user', views.RegistUser.as_view(), name='regist_user'),
    url('app_login', views.AppLogin.as_view(), name='app_login'),
]

 

자 이제 화면에서 로그인을 눌러봅시다.

 

 

로그인 눌러봅시다
화면에 입력한 아이디와 비밀번호가 잘 찍힙니다.

 

클라에서 제대로 데이터가 올라오네요. 그럼 이전에 만들었던 로그인을 기반으로 나머지 로직을 만들어 볼까요?

 

# ~/login/views.py
from django.contrib.auth.hashers import make_password, check_password


class AppLogin(APIView):
    def post(self, request):
        user_id = request.data.get('user_id', "")
        user_pw = request.data.get('user_pw', "")
        user = LoginUser.objects.filter(user_id=user_id).first()

        if user is None:
            return Response(dict(msg="해당 ID의 사용자가 없습니다."))
        
        if check_password(user_pw, user.user_pw):
            return Response(dict(msg="로그인 성공", user_id=user.user_id))
        else:
            return Response(dict(msg="로그인 실패. 패스워드 불일치"))

 

로그인 로직은 간단합니다. 

 

1. 입력받은 user_id를 갖는 사용자가 있는지 확인

2. 있다면 그 사용자의 비밀번호가 입력받은 user_pw와 같은지 확인

3. 로그인 성공

 

주의할 점은 비밀번호를 비교할 때 check_password 함수를 사용해야 한다는 점입니다. 왜냐하면 db에 저장된 패스워드는 암호화된 패스워드이기 때문에 그냥 비교는 안되거든요. 따라서 입력된 user_pw를 암호화해서 비교를 해줘야 하는데 그 기능을 하는 것이 바로 check_password입니다. 

 

로그인 성공
없는 아이디일경우
패스워드가 다를경우

 

 

마치며

 

이번 포스팅부터 실제 클라이언트와 작업하는 느낌(?)을 들기 위해 docker로 만든 react 클라이언트와 함께 작업했습니다. 클라이언트 영역의 소스를 볼 수 없고 어떤 동작을 하는지 모르기 때문에 문서화된 내용을 잘 이해하고 파악하는 게 중요합니다. 또한 중간에 빠진 내용이나 이건 필요할 것 같은데?라는 생각이 들 수 있는데, 이 부분 역시 클라이언트 개발자나 기획자와 얘기하면서 풀어나가야 하는 숙제입니다. 이렇게 많은 커뮤니케이션이 필요하기 때문에 '개발도 역시 사람이 하는 일이다'라는 소리가 나오는 것 같습니다 ㅋㅋ

 

원래는 예제 2번인 To-do 목록 조회까지 같이 하려고 했는데, 생각보다 분량이 많네요. 이것저것 설명하다 보니 같이 하게 되면 너무 많을 것 같아서 분할했습니다 ㅋㅋ 

 

다음 포스팅에서는 To-Do 리스트 조회/생성/삭제/수정에 대해 다뤄보도록 하겠습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY