본문 바로가기

프로젝트/엘리스 AI 트랙

나의 첫 프로젝트(2), "Flask에서 React까지"

[08] 비밀번호 암호화

입력받은 비밀번호 값을 그대로 저장하지 않고 암호화 하여 저장해야하는데 쓰는 함수이다.

해시(hash) 단방향 암호화 기법으로 해시함수(해시 알고리즘)를 이용하여 고정된 길이의 암호화된 문자열로 바꿔버리는 것을 의미한다. 이 과정을 소금 친다고 표현하는데, generate_password_hash는 보통 5만번정도 소금을 친다. (챱챱 x 50000)

 

# app.py

from werkzeug.security import generate_password_hash, check_password_hash

@app.route("/join", methods=["POST"])
def join() :
    data = request.get_json() # axios.post로 받아온 데이터
    user_id = data['user_id']
    user_pw = data['user_pw']
    user_name = data['user_name']

    user_check = User.query.filter(User.user_id == user_id).first()
    if user_check :
        # 중복 아이디 체크하기
        return abort(409)
    
    user_pw = generate_password_hash(user_pw)
    
    new_user = User(user_id, user_pw, user_name)
    
    db.session.add(new_user)
    db.session.commit()

    return jsonify({"result": "success"})
    
@app.route("/login", methods=["POST"])
def login() :
    data = request.get_json()

    user_id = data['user_id']
    user_pw = data['user_pw']

    user_data = User.query.filter(User.user_id == user_id).first()

    if not user_data :
      # 존재하지 않는 아이디입니다.
      abort(400)

    if not check_password_hash(user_data.user_pw, user_pw) :
      # 비밀번호가 일치하지 않습니다.
      abort(401)
 
  # return 에러시 abort를 사용하여 예외처리를 한다.

[09] Abort로 예외처리

http status code를 담아 abort로 exception 처리해주는 것이 좋다. status code로 체크하면 더욱 깔끔하게 체크할 수 있다.

 

▷▶ 해당 err.response.data.message에 따라서 데이터를 각각 불러올 수 있는 기능까지 구현해야 더 깔끔한데, 그부분까지 완성하지는 못했다.

 

# flask

if not user_data :
    # 존재하지 않는 아이디입니다.
    abort(401, "id_not_exist")
    
if not check_password_hash(user_data.user_pw, user_pw) :
    # 비밀번호가 일치하지 않습니다.
    abort(401, "pw_not_correct")

 

/* javascript */

catch(err) {
    console.log(err.response.data);
    console.log(err.response.status);
    console.log(err.response.headers);

    if (err.response.status == 400) {
        alert("이런! 그런 아이디는 없어요")
    }

    if (err.response.status == 401) {
        alert("비밀번호가 일치하지 않아요")
    }
}

// console.log(error)로 찍으면 단순 string만 반환하는데, return 된 object를 받고 싶으면 console.log(error.response)로 받아야 한다.

[10] 서버 URI 환경변수로 지정

 

/* 기존 코드 */
const response = await axios.get("http://localhost:5000/logout")

기존 코드처럼 localhost로 명시하면 나중에 배포할 때 문제가 발생한다. 서버 uri를 환경변수로 설정해서 사용해보자.

 

  • `.env` 파일을 최상위폴더(root)에 만들고, REACT_APP_BASE_URL=http://localhost:5000 작성한 뒤 저장한다.
  • console.log(`REACT_APP_BASE_URL = ${process.env.REACT_APP_BASE_URL}`)찍었을 때 http://localhost:5000가 나오면 성공!!
  • REACT_APP_을 반드시 붙여야 한다.
  • 환경변수를 설정한 뒤 restart해주어야 한다.
  • env.~ 파일은 프로젝트의 최상위 경로에 위치해야 한다.
/* 수정 코드 */
const response = await axios.get(`${process.env.REACT_APP_BASE_URL}/logout`)

 

▷▶ 나중에 서버 배포과정에서 Nginx를 쓰는데 서버 URI를 찾을 수 없어서 결국 .env 파일은 개발단계에서만 쓰고 배포단계에서는 사용하지 않았다. ㅠㅜ


[11] JWT 토큰으로 사용자 인증

엘리스에서 처음 로그인 기능에 대해 배웠을 때는 session["login"] = id 값을 넣어서 서버에 있는 session으로 인증을 하는 방식을 배웠었다. 하지만 session만 쓰기에는 서버에 부담이 커지고 보안이 약하기 때문에 로컬에서 관리할 수 있는 JWT토큰을 새롭게 추가하기로 했다. JWT 토큰은 사용자가 로그인을 할 때 토큰을 발급해줌으로써 서버에서 유저를 확인하기 편리해지고 보안이 높아진다. 유저 정보는 별도의 api에 request를 보내거나 토큰과 함께 가져오는 것이 좋다.

별도의 api에 request를 보내서 가져올 경우에는 GET request에 Authorization header를 넣어 보내고, 서버에서는 @jwt_required 데코레이터로 감싼 함수 내에서 get_jwt_identity()를 통해 현재 유저의 id를 가져올 수 있다. 서버에서 받아온 유저 정보는 상위 component에서 state로 저장한 후 하위 component로 props로 내려주면 효율적이다.

 

냥이 모습을 닮은 JwT [출처: 관짝 고양이]

 

# flask

from flask import Flask
from flask import jsonify
from flask import request

from flask_jwt_extended import create_access_token
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager

app = Flask(__name__)

# JWT를 사용하기 위한 사전작업
app.config["JWT_SECRET_KEY"] = "seeeeeeeeeeeeecret" 
jwt = JWTManager(app)

@app.route("/login", methods=["POST"])
def login():

    data = request.get_json() # axios.post로 보낸 json정보 받아오기
        
    user_id = data['user_id']
    user_pw = data['user_pw']

    user_data = User.query.filter(User.user_id == user_id).first()
    
    # 유저 정보 확인 한 다음 토큰 발급
    access_token = create_access_token(identity=user_data.id) # 유저의 고유한 id값을 JWT토큰안에 넣어주기
    return jsonify({"result": "success", "access_token": access_token})

# JWT로 유저정보 확인하기
# 데코레이터가 있으면 프론트에서 요청시에 JWT헤더를 넣어서 요청해야 접근가능해짐!
@app.route("/protected", methods=["GET"])
@jwt_required() 
def protected():
    current_user = get_jwt_identity()
    return jsonify({"logged_in":current_user}) # 유저의 고유한 id값을 반환


if __name__ == "__main__":
    app.run()

 

/* JavaScript */

// JWT를 사용한 로그인/로그아웃
async function login() {
  	const response = await axios.post(`${process.env.REACT_APP_BASE_URL}/login`, user)
  
  	localStorage.setItem('jwt', response.data["access_token"]) // JWT 토큰 저장
    
    if (response.data["result"] === "success") {
        alert(`안녕하세요 로그인 성공!!!`)
        return history.push("/main")
    }

function logout() {
    localStorage.removeItem('jwt'); // 저장된 토큰 삭제 
}


// 헤더에 JWT넣어서 유저정보 확인하기
async function jwt() {
    const response = await axios.get(`${process.env.REACT_APP_BASE_URL}/protected`, {
        	headers: {
                "Authorization": `Bearer ${localStorage.getItem('jwt')}`,
            }
        })

        return response.data["logged_in"] //유저의 고유 id값 가져오기
    }

 

아버지가 몸이 안좋아지셔서 주말에 본가로 내려왔다. 아빠랑 같이 월요일 아침 일찍 위내시경까지 하러 같이 다녀오느라 피곤했다. 집에서 놀기만하면서 코딩하는걸 게을리 했다. 프로젝트를 어떻게 해야하나 막막했던것도 컸던 것 같다. 그래도 처음 시작한 프로젝트 끝을 봐야지 라는 심정으로 다시 VS code를 켜고 하나하나씩 해보기로했다. 오늘은 저번에 실패한 JWT 구현하기에 재도전하였다. JWT토큰에는 access token, refresh token 두가지가 있는데, access token의 인증시간이 만료되면 refresh token으로 재인증을 받아 access token을 발급해준다. access token 구현에는 성공했지만, refresh token을 넣는것은 아직 구현하지 못했다. 그래서 access token 인증시간이 만료되면 다시 로그인을 해야하는 불편함이 남아 있었다.


[12] 유저 닉네임 가져오기

로그인 했을 때 OOO님 안녕하세요. 라는 문구를 넣고 싶었는데, JWT토큰을 이용하여 유저의 고유 id값을 가져오고,

models.py에 있는 id값으로 유저정보를 찾은다음에 해당하는 닉네임을 불러와 json으로 프론트에 보내줄 수가 있었다!

 

// GET 요청으로 user_name 가져오기
    const [nick_name, setNick_name] = useState("")

    useEffect(() => {
        const bring_data = async() => {
            try {
                const response = await axios.get(`${process.env.REACT_APP_BASE_URL}/nickname`, {
                    headers: {
                        "Authorization": `Bearer ${localStorage.getItem('jwt')}`
                    }
                })
                console.log("닉네임 : " + JSON.stringify(response.data))

                setNick_name(response.data["user_name"])
                
            } catch(err) {
                console.log(err)
            }
          }
          
        bring_data()
    }, [])

 

# 서버에서 JWT토큰이랑 User.id 비교해서 반환하기
@bp.route("/nickname", methods=["GET"])
@jwt_required()
def nickname() :
    query = User.query.filter(User.id == get_jwt_identity()).first()
    data = User.to_dict(query)
    return jsonify(data)

[13] Radio-Button

라디오 버튼을 사용하면 여러가지 항목중에서 하나를 고를 수 있다.

하지만 중복으로 여러개를 쓸 경우 그중에서 하나만 선택되도록 하는 방식을 구현하기 위해서는 name값이 필요하다.

즉, radio button을 중복으로 쓸 경우 name="gender"으로 같은 name값을 줘야지 하나만 선택하도록 할 수 있다.

label for와 input id가 같게 설정하면 동그라미 버튼뿐 아니라 "재학중"이라는 텍스트를 클릭해도 선택할 수 있게된다.

 

▷▶ 로그창에 계속 경고로 떠서 나중에 알고봤더니, JSX에서는 <label for="">이 아닌 <label htmlFor="">을 쓰는게 좋다고 한다!

 

<div>
    <input 
    	id="type01" 
        name="degree" 
        type="radio" 
        value="0" 
        onChange={e => setDegree(e.target.value)} 
    />
    <label htmlFor="type01">재학중</label>
    <input 
    	id="type02" 
        name="degree" 
        type="radio" 
        value="1" 
        onChange={e => setDegree(e.target.value)} 
    />
    <label htmlFor="type02">학사졸업</label>
    <input 
    	id="type03" 
        name="degree" 
        type="radio" 
        value="2" 
        onChange={e => setDegree(e.target.value)} 
    />
    <label htmlFor="type03">석사졸업</label>
    <input 
    	id="type04" 
        name="degree" 
        type="radio" 
        value="3" 
        onChange={e => setDegree(e.target.value)} 
    /> 
    <label htmlFor="type04">박사졸업</label>
</div>

//onChange={e => setDegree(e.target.value)}를 통해서 radio-button state를 변경할 수 있다.

[13] React-Datepicker

react-datepicker를 사용해서 날짜 달력 기능을 손쉽게 만들 수 있다.

설치는 yarn add react-datepicker 또는 npm install react-datepicker 명령어로 설치한다.

▷▶ react-datepicker의 state로 저장한 값을 화면에 보여줄 때 에러가 계속 떴었는데,

JSON.stringify(start_date).slice(1,11)와 같이 json에서 문자열로 바꾼 뒤 내가 원하는 부분만 슬라이싱해서 나타내었다.

조금만 더 코드를 다듬으면 년/월/일로 바꿀 수 있는 부분인데 더 고민해야 하는 부분인 것 같다.

 

import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";

const Date_picker = () => {
    const [startDate, setStartDate] = useState(new Date());
    const [endDate, setEndDate] = useState(new Date())

    return (
        <form>
            <label for="start_date">시작일</label>
            <DatePicker id="start_date" selected={startDate} onChange={(date) => setStartDate(date)} />
            <label for="end_date">종료일</label>
            <DatePicker id="end_date" selected={endDate} onChange={(date) => setEndDate(date)} />
        </form>
    );
}

[14] 최상위 컴포넌트에서 props로 내려받기

react의 매력적인 점은 코드의 재사용이라고 생각했다. 처음 프로젝트를 만들 때는 state나 props에 대해서 깊게 고민하지 않고 코드를 짠 탓에 스파게티같이 엉망이 되었지만, 코드를 리팩토링 하면서 재사용성을 높이기 위해 props를 적극적으로 써보기로 했다. 유저 닉네임 같이 웹사이트 전반적으로 쓰이는 변수는 최상위 컴포넌트에서부터 props로 내려받아서 쓰면 편리해진다.

한편, React-Router-Dom에서 props 넘겨줄 때 주의할 점이 있었다.

 

// 이렇게 하면 props는 무시당한다
<Route path="/main" component={MainPage} token={id} />

 

// 이렇게 해주는게 Best!!!
<Route
    path="/main" 
    render={() => <MainPage token={id} />}/>

 

메인 페이지 화면이 렌더링 되었을 때, useEffect를 사용하여 유저의 닉네임 변수를 호출하도록 설계할 수 있다.

 

porps가 내려간다아아아아ㅏㅏ

React 컴포넌트가 재렌더링 되는 기준
1. props가 변경 될 때
2. state가 변경 될 때
3. 부모 컴포넌트가 재렌더링 될 때

 

// app.js
function App() {
  const [id, setId] = useState(null)

  useEffect(() => {
    const bring_data = async() => {
      try {
          const response = await axios.get(`${process.env.REACT_APP_BASE_URL}/protected`, {
              headers: {
                  "Authorization": `Bearer ${localStorage.getItem('jwt')}` 
              } // JWT 토큰 권한에 통과하기 위해 헤더에 추가해서 유저 id 받아오기
          })

          setId(response.data["logged_in"])
          // state로 id값 상태 저장하기
      } catch(err) {
          console.log(err)
      }
    }

    bring_data()
  }, [id]) // id state가 바뀔 때마다 다시 mount되도록 해야함
  

  return (
    <BrowserRouter>
        <Switch>
        <Route
          path="/main" 
          render={() => <MainPage id={id} />}/>
        <Route
          path="/network" 
          render={() => <NetworkPage id={id} />}/>
        <Route component={NavPage} />
        </Switch>
    </BrowserRouter>
    
  );
}

export default App;

[15] 레고블록처럼 컴포넌트 갈아끼우기

포트폴리오에 있는 자신의 정보를 수정할 때 수정 페이지를 보여준 다음, 작성완료를 눌렀을 때 수정된 화면을 보여줘야하는 기능을 만들어야 했다. 처음에는 머리로 이해는 가지만 코드로 이걸 어떻게 구현하는건지 막막했는데, 엘리스 코치님이 수정을 완료했을 때 새로운 컴포넌트로 갈아끼우면 된다고 조언해주셨다. 다른 사람의 코드도 참고하면서 레고블록 끼우듯이 react를 바꿔치기하는 방법을 새로 익히게 되었다.  isEdit라는 참 혹은 거짓을 변수로 갖는 boolean형 변수를 이용하여 isEdit라는 변수가 true면 수정 후, false면 수정 전 컴포넌트를 나타내도록 하면 된다! isEdit변수는 유저가 로그인 되어있을 때 계속 true, 로그아웃 되어있으면 계속 false를 나타내도록 하여 본인의 포트폴리오 정보를 확인 할 수 있도록 했다.

 

수정 버튼을 누르면 isEdit가 false로 바뀌고 해당 컴포넌트를 갈아끼우게 된다!!

▷▶ 레고블록처럼 갈아끼우는 방법으로 구현에 성공했을 때 react가 정말정말 신기했고, 멋지다고 느꼈다. 사실 아래의 코드 방법이 깔끔한 코드는 아니지만, 내가 스스로 원리를 이해하고 구현했다는 사실 자체로 뿌듯했다 :)

 

const MajorBox = () => {
    const [isEdit, setIsEdit] = useState(true)

    //////////////////////////////////////
    /// 수정되기 전에 나타나는 컴포넌트 ///
    ////////////////////////////////////

    const [school_name, setSchool_name]   = useState("")
    const [school_major, setSchool_major] = useState("")
    const [degree, setDegree]             = useState("")

    const handleMajor = async(e) => {
        e.preventDefault()
        setIsEdit(true) // 버튼을 누르면 isEdit state 변경
        const major_data = {school_name, school_major, degree}

        try {
            const response = await axios.post(`${process.env.REACT_APP_BASE_URL}/major`, major_data)
            console.log(response)
            console.log(response.data)
            console.log(response.status)

            if (response.data["result"] === "success") {
                alert("저장 완료!")
                
            }

        } catch(err) {
            console.log(err)
        }
    }

    const Edit_major_box = (
        <form onSubmit={handleMajor}>
           <p>수정되기 전에 나타나는 컴포넌트</P>
           <button>작성완료</button>
        </form>
    )

    ////////////////////////////////////
    /// 수정된 이후 나타나는 컴포넌트 ///
    ///////////////////////////////////
    const Show_major_box = (
        <div>
           <p>수정된 이후에 나타나는 컴포넌트</P>
           <button onClick={() => {setIsEdit(false)}}>수정</button>
        </div>
    )

    return (
        <>
            {isEdit ? Show_major_box : Edit_major_box} // 삼항연산자를 이용한 조건부 렌더링
        </>
    )
}

[16] 외래키 넣어주기

사용자의 포트폴리오 사이트에 담긴 정보를 불러올 때, models.py에 명시한 각각의 카테고리를 외래키로 묶어서 불러올 수 있다. 우선, JWT토큰을 헤더에 담아서 GET요청을 보내고, JWT토큰에서 고유 ID랑 해당 테이블의 외래키를 비교해서 user모델 데이터를 가져온다. 모델 데이터는 바로 JSON으로 반환할 수 없기 때문에 models.py에 설정한 to_dict 함수로 데이터를 딕셔너리로 변환시켜서 넘겨주면 된다.

 

▷▶ mySQL 데이터를 확인해보니 DB에 외래키 column값이 NULL로 되어있었다... 알고보니 DB에 외래키를 직접 넣어줘야했었다!!

 

# DB에 저장할 때 외래키(key_id)를 따로 지정해줘야함
key_id = get_jwt_identity()
#JWT토큰에서 고유 ID 받아오기

new_major = Major(school_name, school_major, degree, key_id)

db.session.add(new_major)
db.session.commit()
외래키 연결하기
  • 기존 테이블에 majors = db.relationship('Major', backref='user', lazy=True)
  • 해당 테이블에 key_id = db.Column(db.Integer, db.ForeignKey('user.id'))
# models.py

from db_connect import db

class User(db.Model):
    __tablename__ = 'user'

    id            = db.Column(db.Integer,  primary_key=True, nullable=False, autoincrement=True)
    user_id       = db.Column(db.String(100), nullable=False, unique=True)
    user_pw       = db.Column(db.String(500), nullable=False, unique=True)
    user_name     = db.Column(db.String(100), nullable=False)

    # User 테이블과 외래키 연결
    majors       = db.relationship('Major', backref='user', lazy=True)
	
    # DB에 저장할 때 User(user_id, user_pw, user_name)으로 깔끔하고 편리하게 해준다.
    def __init__(self, user_id, user_pw, user_name) :
        self.user_id  = user_id
        self.user_pw = user_pw
        self.user_name = user_name
    
    # models.py에 있는 데이터는 JSON형식으로 바로 돌려줄 수 없기 때문에 딕셔너리로 변환해주는 함수
    def to_dict(self):
        return {
            'id': self.id,
            'user_id': self.user_id,
            'user_name': self.user_name
        }
        
class Major(db.Model):
    __table_name__ = 'major'
 
    id             = db.Column(db.Integer, primary_key=True, autoincrement=True)
    school_name    = db.Column(db.Text())
    school_major   = db.Column(db.Text())
    degree         = db.Column(db.String(10))
 
    # user데이블의 id를 외래키로 하는 user_id
    key_id = db.Column(db.Integer, db.ForeignKey('user.id')) 
	
    # models.py에도 외래키가 들어가도록 설정해 둔다.
    def __init__(self, school_name, school_major, degree, key_id) :
        self.school_name = school_name
        self.school_major = school_major
        self.degree = degree
        self.key_id = key_id

    def to_dict(self):
        return {
            'school_name': self.school_name,
            'school_major': self.school_major,
            'degree': self.degree
        }

[17] useEffect로 GET요청 받기

포트폴리오에 정보를 입력하고 수정된 화면으로 바뀌는 것까지는 확인이 되었는데, 새로고침을 하면 기존의 정보가 유지되지 않고 초기화 되는 상황이 계속 발생했다. 새로고침을 해도 화면에 사용자가 적은 정보를 다시 불러오게 하기 위해 useEffect로 해당 페이지가 마운트 되었을 때 GET요청으로 보내도록 코드를 구성하여 해결하였다.

 

 axios.post로 데이터를 보내는 방법은 쉽게 했었지만, axios.get으로 데이터를 받는 방법은 생각보다 어려웠다. useEffect를 사용하여 데이터를 가져와 변수에 저장하였지만, useEffect밖에서는 그 변수를 쓸 수 없었다. 결국 state를 활용하여 setState로 변수를 저장해야 렌더링 이후에도, useEffect밖에서도 GET으로 불러온 데이터를 실제로 사용할 수 있게 된다.

 

▷▶ 이로써 서버와 데이터를 주고(post), 받는(get) 방법을 모두 알게 되었다! 만세!!!

 

// jwt토큰을 헤더에 달아서 서버로 보내고 받아온 데이터를 state에 저장하기

// React
useEffect(() => {
        const bring_data = async() => {
            try {
                const response = await axios.get(`${process.env.REACT_APP_BASE_URL}/major`, {
                    headers: {
                        "Authorization": `Bearer ${localStorage.getItem('jwt')}`
                    }
                })
                console.log("GET : " + JSON.stringify(response.data))

                setSchool_name(response.data["school_name"])
                setSchool_major(response.data["school_major"])
                setDegree(response.data["degree"])
                
            } catch(err) {
                console.log(err)
            }
          }
      
          bring_data()
    }, [isEdit]) // isEdit 변경될 때마다 다시 GET 요청

 

# 서버에서 get_jwt_identity()으로 유저 고유 ID를 확인한 다음 외래키로 filter해서 유저를 찾는다.

# Flask
@bp.route("/major", methods=["GET", "POST"])
@jwt_required()
def major() :
    if request.method == "GET" :
        key_id = get_jwt_identity()
        major_data = Major.query.filter(Major.key_id == key_id).first()
        major_data = Major.to_dict(major_data)
        return jsonify(major_data)
    	# 유저 모델을 바로 jsonify할 수 없기 때문에 to_dict함수로 딕셔너리로 반환하여 전송해준다.
        
    else :
        data = request.get_json()

        school_name  = data['school_name']
        school_major = data['school_major']
        degree       = data['degree']
        key_id       = get_jwt_identity()

        new_major = Major(school_name, school_major, degree, key_id)
        
        db.session.add(new_major)
        db.session.commit()

    return jsonify({"result": "success"})