개발자로 후회없는 삶 살기

FLASK PART.서비스 개발 본문

[백엔드]/[Etc]

FLASK PART.서비스 개발

몽이장쥰 2023. 1. 21. 12:44

서론

※ 이 포스트는 다음 교재의 학습이 목적임을 밝힙니다.

https://wikidocs.net/81044

 

2-01 플라스크 기초 다지기

현재 파이보 프로젝트는 `projects/myproject` 디렉터리 아래에 pybo.py 파일만 생성한 상태다. 그런데 이보다 규모를 갖춘 플라스크 프로젝트를 만들고자 한다면 …

wikidocs.net

 

본론

3.1 include 기능 사용

플라스크에는 템플릿 특정 위치에 HTML을 삽입해 주는 include 기능이 있습니다. 삽입하고 싶은 html인 navbar.html을 만들고

 

base.html

<!-- 네비게이션바 -->
{% include "navbar.html" %}
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
{% block content %}
{% endblock %}

include 기능을 이용해 위에서 작성한 navbar.html 파일을 base.html 파일에 삽입하면 base.html 템플릿의 특정 위치에 html을 삽입할 수 있습니다.

 

이렇게 include 기능은 템플릿의 특정 영역을 중복, 반복해서 사용할 경우에 유용합니다. 즉, 중복, 반복하는 템플릿의 특정 영역을 따로 템플릿 파일로 만들고, include 기능으로 그 템플릿을 포함합니다. 이처럼 파일로 따로 관리해야 이후 유지·보수하는 데 유리합니다.

 

3.6 회원가입

★ 보통 입력을 받는 경우는 무조건 DB를 쓰는 것 같습니다. 그러니 입력을 받는다? 바로 모델과 폼을 만들어야겠다고 생각할 수 있어야 합니다. 그리고 전체적인 순서는 모델 > 폼 > views > 템플릿 순서로 구현합니다.

 

 

※ 모델 만들기 순서

모델작성 > 리비전 파일 생성 > 리비전 파일로 db 변경

 

- 회원 모델

질문, 답변 모델외에 회원 정보를 위한 모델이 필요합니다.

모델을 구상하고

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(), nullable=False, unique=True)
    password = db.Column(db.String(), nullable=False)
    email = db.Column(db.String(), nullable=False, unique=True)

회원 모델 클래스를 생성합니다.

 

리비전 파일을 생성하고 최신 리비전 파일로 db를 변경하면 완료입니다.

 

 

- 회원가입 폼

※ 입력 순서

플라스크 폼 작성 > 템플릿에서 폼 입력

 

새로운 플라스크 폼을 만듭니다.

class UserCreateForm(FlaskForm):
    email = EmailField('이메일', validators=[DataRequired(), Email()])
    username = StringField('사용자이름', validators=[DataRequired(), Length(min=3, max=25)])
    password1 = PasswordField('비밀번호', validators=[DataRequired(),\
        EqualTo('password2', '비밀번호가 일치하지 않습니다')])
    password2 = PasswordField('비밀번호확인', validators=[DataRequired()])

 

username = 필수항목, 길이가 3-25사이여야 한다는 유효성 검증 조건을 설정하고
password1, 2 = 비밀번호화 비밀번호확인에 대한 필드로 필수항목이고 두 개의 값이 일치해야 하는 EqualTo 검증이 추가되었습니다. password1 속성에 지정된 EqualTo('password2') 는 password1과 password2의 값이 일치해야 함을 의미합니다.

 

email : 필수항목이고 이메일 검증조건이 추가되어 해당 입력값이 이메일 형식과 일치하는지를 검증합니다. email-validator를 설치해야합니다.

 

 

- 회원가입 구현하기

※ 순서

views에 함수 구현 > 블루프린트 등록 > 회원가입 템플릿 작성

 

계정 모델과 계정 폼이 준비되었으니 회원가입을 위한 블루포인트를 만들어야 합니다. POST 방식으로 계정을 저장하고 GET 방식에는 계정 등록 화면을 출력합니다. username으로 데이터를 조회해서 이미 등록된 사용자인지를 확인합니다. 만약 이미 등록된 사용자라면 flash 오류를 발생시킵니다.

@bp.route('/signup/', methods=('GET', 'POST'))
def signup(): 
    form = UserCreateForm()
    if request.method == 'POST' and form.validate_on_submit():

        # username으로 데이터를 조회해서
        user = User.query.filter_by(username=form.username.data).first()

        # 이미 등록된 사용자인지 확인
        if not user:
            user = User(username=form.username.data,
                        # 암호화 저장
                        password=generate_password_hash(form.password1.data),
                        email=form.email.data)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('main.index'))
        else:
            flash('이미 존재하는 사용자입니다.') # 이미 등록된 경우
    return render_template('auth/signup.html', form=form)

flash는 필드 자체 오류가 아닌 프로그램 논리 오류를 발생시키는 함수입니다. flash로 발생시킨 오류를 템플릿에 표시하는 방법도 있습니다.

 

+ 비밀번호는 폼으로 전달받은 값을 그대로 사용하지 않고 generate_password_hash 함수로 암호화하여 저장합니다. generate_password_hash 함수로 암호화한 데이터는 복호화할 수 없습니다. 그래서 로그인할 때 입력받은 비밀번호는 암호화하여 저장된 비밀번호와 비교해야 합니다.

 

 

-> 회원가입 템플릿

<div class="container">
    <h5 class="my-3 border-bottom pb-2">계정생성</h5>
    <form method="post">
        {{ form.csrf_token }}
        {% include "form_errors.html" %}
        <div class="mb-3">
            <label for="username">사용자 이름</label>
            <input type="text" class="form-control" name="username" id="username"
                   value="{{ form.username.data or '' }}">
        </div>
        <div class="mb-3">
            <label for="password1">비밀번호</label>
            <input type="password" class="form-control" name="password1" id="password1"
                   value="{{ form.password1.data or '' }}">
        </div>
        <div class="mb-3">
            <label for="password2">비밀번호 확인</label>
            <input type="password" class="form-control" name="password2" id="password2"
                   value="{{ form.password2.data or '' }}">
        </div>
        <div class="mb-3">
            <label for="email">이메일</label>
            <input type="text" class="form-control" name="email" id="email"
                   value="{{ form.email.data or '' }}">
        </div>
        <button type="submit" class="btn btn-primary">생성하기</button>
    </form>
</div>

회원가입을 위한 사용자이름, 비밀번호, 비밀번호 확인, 이메일에 해당되는 input 엘리먼트를 추가했습니다. 생성하기 버튼을 누르면 현재 페이지인 /auth/signup/ URL로 요청됩니다. 현재 signup.html을 auth/sighup에서 간 것이니 /auth/signup/ 이 호출됩니다.

 

-> 오류 표시하기

<body>
    {% if form.errors %}
    <div class="alert alert-danger" role="alert">
        {% for field, errors in form.errors.items() %}
        <strong>{{ form[field].label }}</strong>
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
        {% endfor %}
    </div>
    {% endif %}
    <!-- flash 오류 -->
    {% for message in get_flashed_messages() %}
    <div class="alert alert-danger" role="alert">
        {{ message }}
    </div>
    {% endfor %}
</body>

회원가입을 할 때 발생할 수 있는 오류를 표시하도록 해보겠습니다. > 오류는 2가지가 있는데 필드에서 발생하는 오류는 폼 validators 검증에 실패한 경우 표시되고 flask를 거치면서 발생하는 오류는 flash()와 같은 로직에 의해 표시됩니다. 

 

 

-> 호출과정 자세히 보기!

메인페이지에서 계정 생성버튼을 누르면 auth/signup/으로 이동하고 href는 GET 방식이므로 auth/signup.html을 출력하고 form을 템플릿의 변수로 줍니다. > 템플릿에서 form에 데이터를 입력합니다.

@bp.route('/signup/', methods=('GET', 'POST'))
def signup(): 

    return render_template('auth/signup.html', form=form)

생성하기 버튼을 누르면 action이 없으니 현재 페이지의 URL로 form 내용을 POST 방식으로 반환하고 현재 페이지는 html을 들어온 경로로 auth/signup입니다. > auth/signup에서 form을 받아서 모델에 데이터 저장합니다.

 

다른 비밀번호를 넣으면 오류 메시지를 표기하고 제대로 입력하면 User 모델에 값이 들어갑니다.

 

 

 

3.7 로그인 로그아웃

순서를 보면 모델 > 폼 > views 함수 구현 > 템플릿 구현이었는데 여기서는 모델은 이미 만들어져 있으니 폼 먼저 작성합니다.

 

- 로그인

1. 로그인 폼

class UserLoginForm(FlaskForm):
    username = StringField('사용자이름', validators=[DataRequired(), Length(min=3, max=25)])
    password = PasswordField('비밀번호', validators=[DataRequired()])

로그인 시 사용할 UserLoginForm을 만듭니다. username, password 필드를 추가하고 각각 필수 입력 항목으로 지정해 줍니다. 또한 username의 길이는 3~25자로 제한했습니다.

 

2. views 함수 구현

@bp.route('/login/', methods=('GET', 'POST'))
def login():
    form = UserLoginForm()
    if request.method == 'POST' and form.validate_on_submit():
        error = None
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            error = "존재하지 않는 사용자입니다."
        elif not check_password_hash(user.password, form.password.data):
            error = "비밀번호가 올바르지 않습니다."
        if error is None:
            session.clear()
            session['user_id'] = user.id
            return redirect(url_for('main.index'))
        flash(error)
    return render_template('auth/login.html', form=form)

로그인 함수는 signup 함수와 비슷하게 POST 방식에는 로그인을 수행하고, GET 요청에는 로그인 화면을 보여줍니다.

GET 방식으로 form에 받은 데이터를 POST 요청에 의해 로그인을 수행합니다. 로그인 수행을 자세히 보면 username으로 데이터 베이스에 해당 사용자가 있는지 검사하고 없다면 오류를 발생합니다.

> 사용자가 존재한다면 폼 입력으로 받은 비밀번호와 check_password_hash 함수를 사용하여 데이터베이스의 비밀번호와 일치하는지를 비교합니다. ★ 데이터베이스에 저장된 비밀번호는 암호화되었으므로 입력된 비밀번호와 바로 비교할 수 없습니다. 입력 비밀번호는 반드시 check_password_hash 함수로 암호화한 후 데이터베이스의 값과 비교해야 합니다.

> 비교를 완료했다면 플라스크 세션에 사용자 정보를 저장하고 메인 페이지로 리다이렉트 합니다. 세션 키에 'user_id'라는 문자열을 저장하고 키에 해당하는 값은 데이터베이스에서 조회한 사용자의 id 값을 저장했습니다.

 

세션은 request와 같이 플라스크가 자체적으로 생성하여 제공하는 객체로 브라우저가 플라스크 서버에 요청을 보내면 request 객체는 요청할 때마다 새로운 객체가 생성됩니다. 하지만 session은 request와 달리 한번 생성하면 그 값을 계속 유지하는 특징이 있습니다.

★ 세션은 서버에 브라우저별로 생성되는 메모리 공간이라고 할 수 있습니다. 따라서 세션에 사용자 id 값을 저장하면 다양한 URL 요청에 세션에 저장된 값을 읽을 수 있습니다. 예를 들어 세션 정보를 확인하여 현재 요청한 주체가 로그인한 사용자인지 아닌지를 판별할 수 있습니다.

 

=> 쿠키와 세션의 이해

웹 프로그램은 [웹 브라우저 요청 → 서버 응답] 순서로 실행되며, 서버 응답이 완료되면 웹 브라우저와 서버 사이의 네트워크 연결은 끊어집니다. 하지만 수많은 브라우저가 서버에 요청할 때마다 매번 새로운 세션이 생성되는 것이 아니라 동일한 브라우저의 요청에서 서버는 동일한 세션을 사용합니다.

+ 서버가 웹 브라우저와 연결 고리(세션)를 맺는 이유는 쿠키에 해답이 있습니다. 쿠키는 서버가 웹 브라우저에 발행하는 값으로 웹 브라우저가 서버에 어떤 요청을 하면 서버는 쿠키를 생성하여 전송하는 방식으로 응답합니다. 

> 웹 브라우저는 서버에서 받은 쿠키를 저장하고 이후 서버에 다시 요청을 보낼 때는 저장한 쿠키를 HTTP 헤더에 담아서 전송합니다. 그러면 서버는 웹 브라우저가 보낸 쿠키를 이전에 발행했던 쿠키값과 비교하여 같은 웹 브라우저에서 요청한 것인지 아닌지를 구분할 수 있습니다. 이때 세션은 바로 쿠키 1개당 생성되는 서버의 메모리 공간이라고 할 수 있습니다.(세션은 브라우저가 서버를 사용하기 위한 메모리 공간이라고 보면 되겠습니다.)

 

3. 로그인 템플릿

<div class="container">
    <h5 class="my-3 border-bottom pb-2">로그인</h5>
    <form method="post">
        {{ form.csrf_token }}
        {% include "form_errors.html" %}
        <div class="mb-3">
            <label for="username">사용자 이름</label>
            <input type="text" class="form-control" name="username" id="username"
                   value="{{ form.username.data or '' }}">
        </div>
        <div class="mb-3">
            <label for="password">비밀번호</label>
            <input type="password" class="form-control" name="password" id="password"
                   value="{{ form.password.data or '' }}">
        </div>
        <button type="submit" class="btn btn-primary">로그인</button>
    </form>
</div>

내비게이션바에서 로그인을 클릭하면 GET 방식으로 로그인 템플릿을 호출하고 로그인 템플릿에서 로그인을 클릭하면 POST 방식으로 현재 웹 브라우저의 주소인 /auth/login을 요청해 login 함수를 실행할 것입니다.

 

성공적으로 로그인이 진행됩니다.

 

 

 

- 로그아웃

로그인한 후에는 내비게이션 바의 로그인 링크를 로그아웃 링크로 바꿔야 합니다. 반대로 로그아웃 상태에서는 로그인 링크로 바꿔야합니다. 사용자의 로그인 여부는 "session에 저장된 값을 조사"하면 알 수 있습니다. 단순히 session에 저장된 user_id값 여부로 로그인을 확인할 수도 있지만 좀 더 일반적으로 사용할 수 있는 방법도 있습니다.

 

 

-> 로그인 여부 확인

@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')
    if user_id is None:
        g.user = None
    else:
        g.user = User.query.get(user_id)

로그인한 사용자 정보를 조회하여 사용할 수 있도록 함수를 작성합니다. @bp.before_app_request 애너테이션이 적용된 함수는 라우팅 함수보다 항상 먼저 실행됩니다. session 변수에 user_id값이 있으면 데이터베이스에서 사용자 정보를 조회하여 g.user에 저장하고 이후 사용자 로그인 검사를 할 때 session을 조사할 필요가 없이 g.user에 값이 있는지만 확인하면 됩니다.(브라우저 별로 하나만 생성되기 때문에 user_id는 하나뿐이라 로그인 검사 가능) g.user에는 User 객체가 저장됩니다.(User 모델의 데이터라고 보면 이해가 됩니다.)

+ @bp.before_app_request : URL로 호출하는 함수가 아닌 해당 별칭의 view가 불리면 계속 알아서 실행되는 애너테이션

 

 

-> 로그인 로그아웃 표시하기

{% if g.user %}
<ul class="navbar-nav">
    <li class="nav-item ">
        <a class="nav-link" href="#">{{ g.user.username }} (로그아웃)</a>
    </li>
</ul>
{% else %}
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
    <li class="nav-item">
        <a class="nav-link" href="{{ url_for('auth.signup') }}">계정생성</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="{{ url_for('auth.login') }}">로그인</a>
    </li>
</ul>
{% endif %}

{% if g.user %} 코드를 추가하여 사용자의 로그인 유무를 판별할 것입니다. 

 

로그인을 했다면 g.user가 만들어진 상태이므로 username을 표시하고 로그아웃 링크를 보여 줍니다.

 

 

-> 로그아웃 라우팅 함수

@bp.route('/logout/')
def logout():
    session.clear()
    return redirect(url_for('main.index'))

로그아웃 함수에는 세션의 모든 값을 삭제할 수 있도록 session.clear를 합니다. 따라서 session에 저장된 user_id는 삭제될 것이고 알아서 계속 실행되는 load_logged_in_user 함수에서 session의 값을 읽을 수 없으므로 g.user도 None이 될 것입니다.

 

로그아웃을 누르면 네비게이션 바에는 다시 계정생성, 로그인 링크가 나타날 것입니다.

'[백엔드] > [Etc]' 카테고리의 다른 글

Flask PART.상세 기능 만들기  (0) 2023.01.20
Flask PART.플라스크 기초 다지기  (0) 2023.01.18
Flask PART.플라스크 개발 환경 준비  (0) 2023.01.18
Comments