[WEB] Flask로 웹페이지 만들기

과제에서 요구하는 기능은 총 4가지였다.

  1. 회원가입 : username, password를 입력 받아 DB에 추가
  2. 로그인 : username, password를 입력 받아 로그인
  3. 로그아웃 : 로그인 한 상태에서 로그아웃
  4. 정보 수정 : 로그인 한 상태에서 새로운 정보를 입력 받아 수정

 


Database

위의 기능들을 flask를 통해 구현해야 한다. app.py를 작성하기 전에, database.py를 작성하여 DB에 users 테이블을 만들어 주었다.

import sqlite3

conn=sqlite3.connect("database.db")
conn.execute(
    '''
    CREATE TABLE users (username text, password text);
    ''')

conn.close()

database.py

 

sqlite3를 import 해야한다는 것은 위의 블로그를 통해 확인했다.

sqlite3는 별도의 DB 서버가 필요없이 DB 파일에 기초하여 데이타베이스 처리를 구현한 Embedded SQL DB 엔진이다. SQLite는 별도의 복잡한 서버 설치가 필요 없고, 쉽고 편리하게 사용할 수 있다는 점에서 널리 사용되고 있다.

라고 블로그에서 말하고 있다.

database.db까지 만들어주면, 이에 연결해서 table를 생성할 수 있다.


app.py

이제 본격적으로 app.py를 살펴보자.


/Index

@app.route('/')
def index():
    return render_template('index.html')

기본 홈

가장 먼저, endpoint에 아무것도 입력되지 않았을 때 보여주는 기본 페이지를 만들었다. 기본 페이지인 만큼 함수 내부에는 별 게 없고, index.html만 반환해준다.

 

<!doctype html>
<html>
    <head>
        <title>Home</title>
    </head>
    <body>
        {% if session['username'] %}
				<h1>Hello, {{ session['username'] }}!</h1>
				<ul>
            <li><a href="{{ url_for('logout') }}">Log out</a></li>
            <li><a href="{{ url_for('update') }}">Update</a></li>
        </ul>
        {% else %}
				<h1>Welcome to the Jin's Homepage!</h1>
        <ul>
            <li><a href="{{ url_for('signup') }}">Sign up</a></li>
            <li><a href="{{ url_for('login') }}">Log in</a></li>
        </ul>
				<br><br>
        <a href="{{ url_for('read') }}">**Only for check**</a>
        {% endif %}
    </body>
</html>

index.html

 

처음이니 html에 대해 잘 뜯어보자.

먼저 <head>와 <body>로 구성되어 있다. <head>는 HTML 문서에 관한 기본 정보를 포함하고 있는 부분이다. 나는 이 부분에서 <title>로 묶으면서, 화면에는 표시되지 않지만 문서 상단의 제목을 설정해주었다.

이렇게 Home이라고 상단에 표시된다.

다음은 <body>에 대해 살펴보자. <body> 부분은 HTML 문서의 본문에 해당하는 곳으로 실제 화면에 나타나는 내용을 기술하는 부분이다. 여기서는 <h1>, <h2> 등으로 감싸서 제목처럼 글씨를 표시할 수 있고, <p>로 하나의 문단, <li>로 목록을 표시할 수 있다. <li>로 감쌀 때는 그냥 쓰기보다는 <ol>이나 <ul>로 감싸주는데, 여기서는 <ul>를 써서 순서가 없게 (unordered)로 했다.

그리고 파이썬과 같이 if, else문도 사용 가능한데, 끝에 endif를 추가해줘야 한다.

 

이제 기능을 살펴보자. if문에서 session[’username’]이 존재하는지 살펴보고 있다. 뒤에서 나오겠지만, 로그인을 하면 서버(DB)에서 session 값을 저장하게 된다. 따라서 로그인이 되어있는지 아닌지는 session에 ‘username’이 존재하는 지를 확인하면 된다.

 

로그인이 되어있다면 logout, update 페이지로 이동할 수 있게, 로그인이 안 되어 있다면 signup, login 페이지에 이동할 수 있도록 했다. 이 때 사용한 것은 a 태그와 href(hypertext reference) 속성이다. a 태그를 href 속성과 같이 사용한다면, 다른 페이지와 연결해줄 수 있다.

또한, url_for은 flask에서 지원하는 기능으로, 괄호 안의 route 함수에 대한 url 값을 가져온다.

 

(only for check는 과제가 요구하는 기능은 아니었지만, 회원가입된 유저를 확인해서 update나 로그인이 정상적으로 작동하는지 확인하기 위해 username을 출력해주는 기능이다!)

 

로그인 X

로그인되어 있지 않은 상태의 페이지다. sign up, log in 페이지로 이동할 수 있다.

 

로그인되어 있는 상태의 페이지다. 로그인한 username은 jin이다. log out, update 페이지로 이동할 수 있다.

 

이제 기본적인 home에 대해 살펴봤으니, 본격적으로 기능들을 하나씩 살펴보자.


/signup

@app.route('/signup',methods=["GET","POST"])
def signup():
    username = request.form.get("username")
    password = request.form.get("password")

    conn = sqlite3.connect("database.db")
    cur = conn.cursor()

    # 회원가입
    if username and password:
        cur.execute(f"SELECT * FROM users WHERE username = '{username}'")
        result = cur.fetchone()
        # username이 중복 시 error
        if result:
            error = "This username is already taken"
            return render_template('signup.html', error=error)
        
        cur.execute(f"INSERT INTO users values ('{username}', '{password}');")
        conn.commit()
        conn.close()
        return redirect(url_for("index"))
    
    conn.commit()
    conn.close()
    return render_template('signup.html')

signup

signup 함수이다. 이는 /signup으로 요청이 들어오면 실행된다.

먼저 username과 password는 requset의 form 속성을 활용하여 클라이언트가 서버에 보낸 POST에서 각각 username과 password에 해당하는 값을 얻어온다.

 

conn 변수를 선언하여 database.db와 연결한다. 그리고 연결한 conn을 이용해 cur 객체를 생성한다. 커서는 데이터베이스에서 SELECT, INSERT, UPDATE, DELETE 등의 SQL 문을 실행하고 결과를 반환한다.

 

그 다음 if문을 통해 username과 password 중 none 값이 없다면 username을 cur 객체를 활용해 select해온다. 그리고 이 결과를 result에 저장하는데, fetchone을 활용하여 만약 select로 값을 얻어왔다면 쿼리의 첫 튜플 값이, 얻어오지 못했다면 nonedl 저장된다.

얻어온 값이 있다는 것은 이미 존재하는 username이므로 error에 메시지를 저장하고, signup.html과 함께 반환한다. render_template은 signup.html을 렌더링해서 보여준다는 의미이다.

 

만약 중복되지 않는다면 다시 cur 객체를 통해 username과 password를 users table에 추가하고, 이 결과를 커밋, 마지막으로 DB와의 연결을 종료한다. 그리고 redirect(url_for(’index’))를 통해 url 페이지를 다시 로드시켜 주는데, 여기서 redirect는 클라이언트의 요청을 다른 url로 보내주는 것으로, signup이 끝났기에 index 페이지에 요청을 보내 index 페이지를 보여주는 것이다.

 

그리고 만약 username이나 password 중 하나가 none이라면 그냥 계속 signup 페이지에 머물게 된다.

 

<!doctype html>
<html>
    <head>
        <title>Signup</title>
    </head>
    <body>
        <h1>Sign up</h1>
        {% if error %}
            <p style="color:red;">{{ error }}</p>
        {% endif %}
        
        <form method="POST">
            <label for="username">Username:</label>
            <input name="username" type="text" id="username" value="" placeholder="Enter your username" required>
            <br><br>
            <label for="password">Password:</label>
            <input name="password" type="password" id="password" value="" placeholder="Enter your password" required>
            <br><br>
            <input type="submit" value="Sign up">
        </form>
    </body>
</html>

signup.html

signup.html이다. 제목으로는 signup을 가지고 있다. 만약 error 메세지가 있다면, 받은 error 메세지를 빨간색으로 출력해준다!

아래는 form을 구성하여 POST에 들어갈 값을 입력받도록 보여준다. label은 UI를 좀 더 좋게 해주는 것인데, 입력 받는 칸 옆에 Username: 이 표시되고, 이를 클릭하면 입력 받는 칸으로 이동하게 된다.

input은 사용자에게 입력받도록 한다. 여기에 이름, 타입, id, value, placeholder 등을 선언할 수 있는데, id는 고유값 속성으로 javasciprt나 css에서 참조할 수 있게하며, placeholder는 입력 값의 예시처럼 칸에 표시해주는 것이다. 추가로 password의 type은 password로 하여 실제로 입력하는 값을 화면에 보여주지 않는다.

 

<br>은 줄바꿈이다.

마지막으로 입력을 누르는 부분의 값을 Sign up으로 하였다.

 

html 페이지는 다음과 같이 구성된다.

 

만약 중복되는 username을 입력할 시 위와 같이 빨간 글씨로 error 메시지를 보여준다!

 


/login

@app.route('/login',methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get("username")
        password = request.form.get("password")

        conn = sqlite3.connect("database.db")
        cur = conn.cursor()

        #로그인
        cur.execute(f"SELECT * FROM users WHERE username= '{username}' AND password='{password}';")
        user = cur.fetchone()
        conn.close()
        
        # username이 유효
        if user:
            session['username'] = user[0]
            return redirect(url_for('index'))
        # 해당 username 없음
        else:
            error = 'Invalid username or password. Please try again.'
            return render_template('login.html', error=error)
    return render_template('login.html')

login

로그인하는 부분을 구현한 것이다. /login이 입력되면 해당 페이지를 보여준다.

signup과 같게 username과 password를 받아오고 database에 연결, cur 객체를 만든다.

그 다음 users table에서 클라이언트가 입력한 username을 가져오고 성공 여부를 user에 저장한다. 만약 user가 none이 아니라면 session에 username에 해당하는 값을 추가한다.

flask의 session을 사용하는 방법은 위의 블로그를 참고하였다.

추가한 다음 index 페이지를 가져온다.

 

만약 username이 없다면 error 메시지와 함께 login 페이지를 다시 보여준다.

 

<!DOCTYPE html>
<html>
<head>
	<title>Login</title>
</head>
<body>
	<h1>Login</h1>
	{% if error %}
		<p style="color:red;">{{ error }}</p>
	{% endif %}
	<form method="post">
		<label for="username">Username:</label>
		<input type="text" id="username" name="username" required><br><br>
		<label for="password">Password:</label>
		<input type="password" id="password" name="password" required><br><br>
		<input type="submit" value="Login">
	</form>
	<br>
	<p>Don't have an account? <a href="{{ url_for('signup') }}">Sign up here</a>.</p>
</body>
</html>

login.html

login.html이다. title은 로그인이고, 나머지 부분은 signup과 똑같다.

그런데 이번엔 보너스로 맨 밑에 계정이 없다면 회원가입을 하러 바로 가게 만들어주었다. signup의 url을 불러오는 href가 적용된 것을 확인할 수 있다.

 

로그인 페이지는 이렇게 보여진다.

존재하지 않는 username을 입력하면 이렇게 빨간 글씨로 에러 메시지를 출력해준다!

 


/logout

@app.route('/logout')
def logout():
    # 로그 아웃 - session에서 username 삭제
    session.pop('username', None)
    return redirect(url_for('index'))

logout

logout 기능을 구현한 것이다. logout에는 많은 기능이 필요하지는 않다. 단지 session에 저장된 username의 값을 pop하여 제거해주기만 하면 된다! logout 이후에는 index 페이지를 다시 불러온다.

jin으로 로그인되어 있는 상태이다.

 

로그아웃을 눌렀더니 다시 index 페이지를 불러왔고, 로그인된 상태가 아니므로 signup, login 메뉴를 보여주는 것을 확인할 수 있다!

원래는 session에 username의 특정 값을 저장함으로써 로그인 상태를 유지하였는데, login 상태의 index 페이지에서 logout을 누르면 logout 함수를 실행하면서 바로 session에서 값이 삭제되고, 이후에는 서버 입장에서 해당 username에 대한 값이 없어 로그인하지 않는 상태라고 인식하게 되는 것이다.

 


 

/update

@app.route('/update', methods=['GET', 'POST'])
def update():
    if 'username' not in session:
        return redirect(url_for('login'))

    if request.method == 'POST':
        new_username = request.form.get('username')
        new_password = request.form.get('password')

        conn = sqlite3.connect("database.db")
        cur = conn.cursor()

        # username이 이미 존재하는지 확인
        cur.execute(f"SELECT * FROM users WHERE username = '{new_username}'")
        result = cur.fetchone()
        if result:
            error = "This username is already taken"
            conn.close()
            return render_template('update.html', error=error)

        # 정보 수정
        username = session['username']
        cur.execute(f"UPDATE users SET username='{new_username}', password='{new_password}' WHERE username='{username}'")
        conn.commit()
        conn.close()

        session['username'] = new_username
        return redirect(url_for('index'))

    return render_template('update.html')

update

마지막으로 정보 수정 기능이다. 물론 로그인된 상태의 index 페이지에서만 보여준다 그러긴 했지만, 직접 url을 검색하여 이동하는 것을 생각해서 처음에 username이 session에 존재하지 않는다면 로그인 페이지를 가져오도록 했다.

 

그 다음 이전과 같이 new_username과 new_password를 POST에서 받아오고, DB와 연결하였다.

query를 통해 new_username이 users table에 존재하는지 확인하고, 중복된다면 error 메시지와 함께 update.html를 렌더링하여 보여주었다.

 

중복되지 않는다면 session에서 username을 받아오고, users table의 username과 password를 update, set구문을 통해 업데이트 해준다. 그 다음 세션의 username 값을 new_uesrname으로 바꿔주고 index 페이지를 다시 불러온다.

 

<!doctype html>
<html>
    <head>
        <title>Update</title>
    </head>
    <body>
        <h1>Update</h1>
        {% if error %}
            <p style="color:red;">{{ error }}</p>
        {% endif %}
        <form method="POST">
            <label for="username">New Username:</label>
            <input type="text" id="username" name="username"><br><br>
            <label for="password">New Password:</label>
            <input type="password" id="password" name="password"><br><br>
            <input type="submit" value="Submit">
        </form>
    </body>
</html>

update.html

update.html이다. 제목이 다른 것과 new가 붙은 것 빼고는 login과 다를 게 없다.

 

jin으로 로그인되어 있다.

update 페이지이다.

 

이미 존재하는 username으로는 바꿀 수 없다.

 

cykor로 바꿔보았다.

username이 바뀌었다!


최종적으로 정리해보자.

먼저 database.db를 만들어서 DB를 파일로 관리할 수 있고, database.py를 통해 DB에 table을 생성해줬다.

그 다음 app.py에 여러 기능들을 구현해놓고, 기능들을 보여줄 페이지를 html로 만들었다.

사용자가 브라우저에 주소와 endpoint에 특정 route를 적으면, 내가 함수에 작성한 @app.route에 맞게 함수가 Flask에 의해 실행된다. 보통 여기서 각 함수는 html을 return해준다. html은 사용자에게 보여주는 인터페이스를 구성하고 있다.

만약 함수가 사용자의 입력값을 참조한다면, POST를 구성하는 form 속성에서 사용자의 입력값을 따와서 이를 함수에 사용하게 된다. 이번 과제에서는 함수의 입력값으로 중복을 검사하는 쿼리나 DB에 새로 추가하는 쿼리 등에 활용했다. 또, 로그인 상태를 유지하기 위해 session에 정보를 저장하는 데에도 사용하였다. 그리고 이렇게 입력을 받을 수 있는 것은 html에서 입력을 받는 칸을 만들어두었기 때문이다.

함수는 렌더링한 html을 리턴하여 사용자에게 보여주게 된다.

 

++ 부가기능

@app.route('/read')
def read():
    conn = sqlite3.connect("database.db")
    cur = conn.cursor()
    cur.execute(f"SELECT * FROM users;")
    rows = cur.fetchall()
    print(rows)
    conn.commit()
    conn.close()
    return render_template('view.html', posts=rows)

 

username만 체크하는 부분,, 회원가입이나 로그인, 정보 수정 등이 적절하게 이루어지는 지 확인하기 위해 만들어두었다.

<!doctype html>
<html>
    <head>
        <title>View</title>
    </head>
    <body>
        <h1>Users</h1>
        <table>
        <tr>
            <td> Username </td>
        </tr>
        {% for post in posts %}
        <tr>
            <td> {{ post[0] }} </td>
        </tr>
        {% endfor %}
        </table>
    </body>
</html>

view.html

 

DB에 저장된 username들을 보여준다. 만약 중복된 username이라고 한다면 여기에 있는지 슬쩍 확인해 볼 수 있다 ^^

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤