React, DRF API를 이용한 velog 따라 만들어보기 3장

大 炫 ·2020년 9월 10일
5

Velog Clone

목록 보기
3/8
post-thumbnail

시작하기 앞서

3장의 내용은 2장의 backend에서 해놓은 세팅을 활용,

  1. 회원가입시 user의 토큰발행과,
  2. 회원가입이 되있는 username에 대해서는 중복이 불가능하기에 "사용할 수 없는 아이디"라는 alert창,
  3. 존재하는 username에 password, 회원가입시 얻은 토큰이 일치할 시 로그인을 성공하여 profile을 받아오고, 프로필을 보고, 수정까지 할 수 있는 Profile.js영역 추가,
  4. 토큰의 유효기간이 끝난 user에 대해서는 토큰의 재발행, 및 만료기간 연장
  • 선택사항으로 oauth 서비스 기능을 이용한 react-google-login 인증,
  • Insomnia같은 tool을 이용해 front와 back의 세팅 중 하나만 완료 되었을 때, 알맞게 해놓았는지 테스트 해볼 수 있는 간단한 이용법에 대해서 다루려고 한다.

그럼 먼저 토큰발행을 위한 회원가입과 로그인, 소셜로그인이 없다는 전제하에 LoginModal.js를 살펴보자.

JWT 토큰 발행

frontend/src/components/LoginModal.js

import React, { useState } from 'react';
import { useHistory } from 'react-router'
import '../css/LoginModal.css';
// import GoogleLogin from 'react-google-login';

function LoginModal(props){
  let [JoinLoign,setJoinLogin] = useState('로그인')
  const history = useHistory()

  let [username, setUsername] = useState()
  let [userpassword, setUserPassword] = useState()
  
  const data = {username : username, password : userpassword}

  const handleNameChange = (e) => {
    setUsername(e.target.value)
  }
  const handlePasswordChange = (e) => {
    setUserPassword(e.target.value)
  }
  return(
    <>
      <div className="login-container">
        <div className="login-box">
          <div className="exit">
              <button onClick={()=>{ history.goBack() }}>
                <svg stroke="currentColor" fill="currentColor" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>
              </button>
          </div>
          <span>{JoinLoign}</span>
          <form>
            {
              JoinLoign === '로그인'
              ?(
                <>
                <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
                <input type="password" placeholder="비밀번호를 입력하세요" id="password" onChange={handlePasswordChange}/>
                <button className="JoinLoign-button" onClick={(e)=>{
                e.preventDefault()
                  fetch('http://localhost:8000/login/', {  
                  method: 'POST',
                    headers: {
                      'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                  })
                  .then(res => res.json())
                  .then(json => {
                    // user data와 token정보가 일치하면 로그인 성공
                    if (json.user && json.user.username && json.token) {
                      props.userHasAuthenticated(true, json.user.username, json.token);
                      history.push("/");
                      props.setModal(true)
                    }else{
                      alert("아이디 또는 비밀번호를 확인해주세요.")
                    }
                  })
                  .catch(error => alert(error));
                }}>{JoinLoign}</button>
                </>
              )
              :(
                <>
                <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
                <input type="password" placeholder="비밀번호를 입력하세요" onChange={handlePasswordChange}/>
                <button className="JoinLoign-button" onClick={(e)=>{
                e.preventDefault()
                  fetch('http://localhost:8000/user/', {
                    method: 'POST',
                    headers:{
                      'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                  }).then(res => res.json())
                  .then(json => {
                    if (json.username && json.token) {
                      props.userHasAuthenticated(true, json.username, json.token);
                      history.push("/");
                      props.setModal(true)
                    }else{
                      alert("사용불가능한 아이디입니다.")
                    }
                  })
                  .catch(error => alert(error));
                }}
                >{JoinLoign}</button>
                </>
              )
            }
            
          </form>
          <div className="login-foot">
            {
              JoinLoign === '회원가입'
              ?
              (
                <>
                <span>이미 회원이신가요  ?</span>
                <div className="foot-link" onClick={(e)=>{
                e.preventDefault()
                setJoinLogin('로그인')
                }}>로그인</div>
                </>
              )
              :
              (
                <>
                <span>아직 회원이 아니신가요 ?</span>
                <div className="foot-link" onClick={(e)=>{
                e.preventDefault()
                setJoinLogin('회원가입')
                }}>회원가입</div>
                </>
              )
            }
          </div>
        </div>
      </div>
    </>
  )
}
export default LoginModal;

우선 1장과 비교하여 달라진 코드들을 뜯어서 하나씩 설명해 보자.

function LoginModal(props){
...
}

함수 LoginModal은 props라는 데이터를 상위 component로부터 바운딩받은 것이다. LoginModal의 상위 Component인 App.js의 코드를 살펴보면 이와같다.

...
function App(){
  const userHasAuthenticated = (authenticated, username, token) => { 
    setisAuthenticated(authenticated)
    setUser(username)
    localStorage.setItem('token', token);
  }//회원가입이나 로그인이 성공했을 때 토큰을 저장
  ...
	
    return(
    	...
        <Route exact path="/login">
            <LoginModal setModal={setModal} userHasAuthenticated={userHasAuthenticated}/>
        </Route>
    )
}

App.js에서 LoginModal을 향해 modal의 상태값을 변경할 수 있는 함수 setModal을 setModal이라는 이름으로 바운딩 시킨다.
또 userHasAuthenticated라는 토큰을 저장하는 함수를 같은이름으로 데이터 바운딩시킨다는 의미인데 이는 위의 LoginModal(props)에 props에 담기게된다.
확인하고 싶다면 항상 console.log를 찍어보길 바란다.

console.log(props)

이와같은 코드를 LoginModal.js에 추가해서 콘솔창을 확인할 경우 우리는

이와같은 결과값이 출력되는것을 확인 할 수 있다. 직접 코드를 짜는 경우에도 항상 중간중간에 log를 찍어서 값이 정상출력되는 것을 확인하지만 나에겐 타인이 짜놓은 코드의 log를 찍어보는 것 만큼 직관적으로 데이터를 확인할 수 있는 최상의 방법이라고 생각한다.
나같은 코린이에게 늘 추천하고 싶은 방식이다 !

...
  return(
  ...
              <form>
            {
              JoinLoign === '로그인'
              ?(
                <>
                <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
                <input type="password" placeholder="비밀번호를 입력하세요" id="password" onChange={handlePasswordChange}/>
		...
                </>
              )
              :(
                <>
                <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
                <input type="password" placeholder="비밀번호를 입력하세요" onChange={handlePasswordChange}/>
		...
                </>
              )
            }
          </form>
          ...
  )

JoinLoign(이라는 state값) === '로그인' 참이라면 ?() 거짓이라면 :()이라는 삼항연산자를 이용해서 각각의 state값에 해당할 때 출력되는 내용이 다르도록 설정해준 모습이다.

form태그안에 회원가입과 로그인의 input을 살펴보자.

모든 input에는 onChange라는 이벤트가 존재하는데 이는 각각의 이벤트에 handleNameChange, handlePasswordChange라는 이벤트 핸들러가 실행될 수 있도록 담아준 모습이다.
쉽게말해 input안에 사용자가 정보를 기입하는 행위(이벤트)가 발생할 때 각각의 이벤트핸들러를 실행시켜라는 의미인데 그럼 이벤트 핸들러와 어떤 onChange가 일어나는지 살펴보자.

  let [username, setUsername] = useState()
  let [userpassword, setUserPassword] = useState()
  
  const data = {username : username, password : userpassword}
  
  const handleNameChange = (e) => {
    setUsername(e.target.value)
  }
  const handlePasswordChange = (e) => {
    setUserPassword(e.target.value)
  }
  

사용자가 기입하는 정보를 담으려고 우선 username, userpassword이라는 state값을 생성했고,
그걸 setState할 수 있는 함수인 setUsername과 setUserPassword라는 함수를 작성해준 모습이다.
그리고 onChange에들어가는 이벤트 핸들러를 살펴보면 handleNameChange라는 함수를 선언할건데 이함수는 (e)라는 매개변수로 setUsername을 e.target.value로 바꾼다는 내용이다.
handlePasswordChange또한 같은 맥락이다.

화살표함수가 익숙하지 않다면 이렇게 본다면 더 이해가 빠를것이다.

function handlePasswordChange(e){
    setUserPassword(e.target.value)
  }

그럼 e? e.target? e.target.value ?? 무엇을 지칭하는것일까 ?
console.log에 찍어보도록 하겠다.

const handleNameChange = (e) => {
setUsername(e.target.value)
console.log(e)
console.log(e.target)
console.log(e.target.value)
}


console창에서 각각의 log를 본다면 e는 input에 정보를 기입하는 사용자의 행위, 즉 이벤트 자체를 뜻하고 target은 이벤트가 일어나는 e.target즉 input태그 자체를 지칭한다.
마지막으론 그 target이 가진 value값인 e.target.value !
state값 또한 정확하게 setState되었는지 살펴보자.

해당하는 Component의 state값을 실시간으로 볼 수있는 tool은 chrom에서 지원하는 React Developer Tools을 통해서 확인할 수 있다.
가장위의 state값으로 로그인부터 입력한 username값과 password또한 state에 잘 담긴것을 확인할 수 있다. 그렇다면 회원가입 버튼을 누르면 어떤식으로 바뀌는지 확인해보자.

회원가입을 클릭시 state값이 "로그인"에서 "회원가입"으로 변했기 때문에 모든 "로그인"들이 "회원가입"으로 바뀐모습을 확인할 수 있는 것이다.
앞서 로그인input에서 작성한 value값도 따라왔는데 이게 거슬린다면 각각의 input에 다른 state값을 줘도되고 회원가입과 로그인을 onClick했을 때 작성한 username과 password의 state값을 ""빈 문자열로 초기화 시키는 등 다양한 방법이 많으니까 원하는 방법으로 해보길 바란다!

마지막으로 LoginModal의 가장핵심인 로그인이든 회원가입이든 서버로 보내는 코드만이 남았는데, 모르는 내용이 많아서 나도 처음 접했을 때 엄청 어렵게 느껴졌지만 몇번 사용하다보면 서버로 데이터를 보낼때는 정확한 형식에 맞춰서 보내야하기 때문에 틀을 벗어나면 데이터를 보낼 수 없는데 이 때문에 데이터를 보내는, 가져오는 코드가 서로 크게 다르지 않아 금방 익숙해지더라.
그럼 먼저 로그인을 살펴보자

            JoinLoign === '로그인'
            ?(
              <>
              <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
              <input type="password" placeholder="비밀번호를 입력하세요" id="password" onChange={handlePasswordChange}/>
              <button className="JoinLoign-button" onClick={(e)=>{
              e.preventDefault()
                fetch('http://localhost:8000/login/', {  
                method: 'POST',
                  headers: {
                    'Content-Type': 'application/json'
                  },
                  body: JSON.stringify(data)
                })
                .then(res => res.json())
                .then(json => {
                  // user data와 token정보가 일치하면 로그인 성공
                  if (json.user && json.user.username && json.token) {
                    props.userHasAuthenticated(true, json.user.username, json.token);
                    history.push("/");
                    props.setModal(true)
                  }else{
                    alert("아이디 또는 비밀번호를 확인해주세요.")
                  }
                })
                .catch(error => alert(error));
              }}>{JoinLoign}</button>
              </>
            )

나는 button의 onClick을 이용해서 실행시켰지만 코드를 form로 감싸 button의 타입은 submit form의 onSubmit의 이벤트를 이용해도 무방하다.(리팩토링을 시작하게되면 그렇게 고칠 예정이다.)
중요한 부분은 fetch, .then 함수이다.
우선 fetch는 리액트에서 네트워크 통신을 도와주는 api인데 fetch말고도 거의 유사한 기능인 axios도 존재한다. 각각 장점이 존재하기에 입맛에 따라 쓰면 좋기때문에 둘 다 알아놓는 편이 좋을 듯하다 !
fetch와 axios, 모두 Promise객체를 return하는데 이 때 .then은 fetch의 return값을 다시 매개변수로 받는 callback함수라고 보면된다.
이에 관한 자료로 이곳 을 참조하며 이해에 상당한 도움이 됬으니 읽어보면 좋을 듯하다.
그래서 다시 코드를 살펴보면

fetch('http://localhost:8000/login/', {  
                  method: 'POST',
                    headers: {
                      'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                  })

'http://localhost:8000/login/' 라는 url에 POST요청을 하는데 content-type은 'application/json'이고 body에 담아 보내는 내용JSON.stringify(data)data라는 객체를 JSON형태로 변환(서버가 알아들을 수 있는 언어)해서 POST를 요청한다는 내용이다. 이때 data라는 객체는 이렇게 정의해놨다.

  let [username, setUsername] = useState()
  let [userpassword, setUserPassword] = useState()
  
  const data = {username : username, password : userpassword}

여기서 backend의 내용을 다시 짚고 넘어가보자.

backend/user/views.py

class UserList(APIView):

    permission_classes = (permissions.AllowAny,)

    def post(self, request, format=None):
        serializer = UserSerializerWithToken(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

해당 url의 POST Method는 self, request, format=None 이라는 매개변수를 받는데 이때

if serializer.is_valid(): 

이 참이라면 return되는 결과값에 Response값에 주목해보자.
각각의 함수와 객체가 정확히 무엇을 가르키는지는 모르겠지만 우리가 backend에서 올바른 request를 받았을 때 그에대한 응답값으로 Response를 반환한다면 그것이 바로 토큰이다.

다시 frontend로 돌아가보자. 그럼 fetch가 올바른 요청을 했다라는 가정하에return값인 Promise객체는 분명 토큰값을 받았을 것이다. 이제 .then이 나와야한다.

.then(res => res.json())
.then(json => {
	// user data와 token정보가 일치하면 로그인 성공
	if (json.user && json.user.username && json.token) {
	props.userHasAuthenticated(true, json.user.username, json.token);
	history.push("/");
	props.setModal(true)
	}else{
	alert("아이디 또는 비밀번호를 확인해주세요.")
	}
})
.catch(error => alert(error));

서버에서 받은 res값을 res.json()을 통해 front에서 알아들을 수 있는 언어로 변환하고 json에 담긴 user의 data와 token의 정보가 일치한다면 로그인에 성공한다는 내용이다.
props.userHasAuthenticated에는 회원가입이나 로그인이 성공했을 때 localStorage에 토큰을 저장하는 함수이며 App.js에 정의된,즉 App.js로부터 바운딩받은 함수라서 앞에 props를 붙여준 모습이다.

App.js를 살피기전 Google-login기능까지 넣은 LoginModal.js 코드는 직접 분석하며 공부해보길 권장한다. 이곳oauth의 기능인 소셜로그인에 대해 설명한다.

frontend/src/components/LoginModal.js + React-google-login

import React, { useState } from 'react';
import { useHistory } from 'react-router'
import '../css/LoginModal.css';
import GoogleLogin from 'react-google-login';

function LoginModal(props){
  let [JoinLoign,setJoinLogin] = useState('로그인')
  const history = useHistory()

  //구글 아이디로 회원가입 및 이미 회원일경우
  let responseGoogle = (res)=>{
    let email = res.profileObj.email
    let id_token = res.profileObj.googleId

    let data = {
      username: email,
      password: id_token,
      provider: 'google'
    }

    fetch('http://localhost:8000/user/', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    })
    .then(res => res.json())
    .then(json => {
      if (json.username && json.token) {
        props.userHasAuthenticated(true, json.username, json.token);
        props.setModal(true);
        history.push("/");
      }else{
        // 서버에 Google 계정 이미 저장돼 있다면 Login 작업 수행
        // 로그인을 시도하기 전에 서버에 접근하기 위한 access token을 발급 받음
        fetch('http://localhost:8000/login/', {  
        method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data)
        })
        .then(res => res.json())
        .then(json => {
          // 발급 완료 되었다면 해당 토큰을 클라이언트 Local Storage에 저장
          if (json.user && json.user.username && json.token) {
            props.userHasAuthenticated(true, json.user.username, json.token);
            props.setModal(true);
            history.push("/");
          }
        })
        .catch(error => {
          console.log(error);
          window.gapi && window.gapi.auth2.getAuthInstance().signOut();
        });   
      }
    })
    .catch(error => {
      console.log(error);
      window.gapi && window.gapi.auth2.getAuthInstance().signOut();
    });  
  }//구글 아이디로 회원가입 및 이미 회원일경우


  let [username, setUsername] = useState()
  let [userpassword, setUserPassword] = useState()
  
  const data = {username : username, password : userpassword}

  const handleNameChange = (e) => {
    setUsername(e.target.value)
  }
  const handlePasswordChange = (e) => {
    setUserPassword(e.target.value)
  }

  return(
    <>
      <div className="login-container">
        <div className="login-box">
          <div className="exit">
              <button onClick={()=>{ history.goBack() }}>
                <svg stroke="currentColor" fill="currentColor" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>
              </button>
          </div>
          <span>{JoinLoign}</span>
          <form>
            {
              JoinLoign === '로그인'
              ?(
                <>
                <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
                <input type="password" placeholder="비밀번호를 입력하세요" id="password" onChange={handlePasswordChange}/>
                <button className="JoinLoign-button" onClick={(e)=>{
                  e.preventDefault()
                  fetch('http://localhost:8000/login/', {  
                  method: 'POST',
                    headers: {
                      'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                  })
                  .then(res => res.json())
                  .then(json => {
                    // user data와 token정보가 일치하면 로그인 성공
                    if (json.user && json.user.username && json.token) {
                      props.userHasAuthenticated(true, json.user.username, json.token);
                      history.push("/");
                      props.setModal(true)
                      console.log(json)
                    }else{
                      alert("아이디 또는 비밀번호를 확인해주세요.")
                    }
                  })
                  .catch(error => alert(error));
                }}>{JoinLoign}</button>
                </>
              )
              :(
                <>
                <input type="text" placeholder="아이디를 입력하세요" onChange={handleNameChange}/>
                <input type="password" placeholder="비밀번호를 입력하세요" onChange={handlePasswordChange}/>
                <button className="JoinLoign-button" onClick={(e)=>{
                  e.preventDefault()
                  fetch('http://localhost:8000/user/', {
                    method: 'POST',
                    headers:{
                      'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                  }).then(res => res.json())
                  .then(json => {
                    if (json.username && json.token) {
                      props.userHasAuthenticated(true, json.username, json.token);
                      history.push("/");
                      props.setModal(true)
                    }else{
                      alert("사용불가능한 아이디입니다.")
                    }
                  })
                  .catch(error => alert(error));
                }}
                >{JoinLoign}</button>
                </>
              )
            }
            
          </form>
          <section className="social-box">
            <h4>소셜 계정으로 {JoinLoign}</h4>
            <div className="googlebox">
              <GoogleLogin
                render={renderProps => (
                  <button onClick={renderProps.onClick} disabled={renderProps.disabled}>
                    <svg width="20" height="20" fill="none" viewBox="0 0 20 20" className="google-login">
                      <path fill="#4285F4" d="M19.99 10.187c0-.82-.069-1.417-.216-2.037H10.2v3.698h5.62c-.113.92-.725 2.303-2.084 3.233l-.02.124 3.028 2.292.21.02c1.926-1.738 3.037-4.296 3.037-7.33z"></path>
                      <path fill="#34A853" d="M10.2 19.931c2.753 0 5.064-.886 6.753-2.414l-3.218-2.436c-.862.587-2.017.997-3.536.997a6.126 6.126 0 0 1-5.801-4.141l-.12.01-3.148 2.38-.041.112c1.677 3.256 5.122 5.492 9.11 5.492z"></path>
                      <path fill="#FBBC05" d="M4.398 11.937a6.008 6.008 0 0 1-.34-1.971c0-.687.125-1.351.329-1.971l-.006-.132-3.188-2.42-.104.05A9.79 9.79 0 0 0 .001 9.965a9.79 9.79 0 0 0 1.088 4.473l3.309-2.502z"></path>
                      <path fill="#EB4335" d="M10.2 3.853c1.914 0 3.206.809 3.943 1.484l2.878-2.746C15.253.985 12.953 0 10.199 0 6.211 0 2.766 2.237 1.09 5.492l3.297 2.503A6.152 6.152 0 0 1 10.2 3.853z"></path>
                    </svg>
                  </button>
                )}
                buttonText=""
                clientId="161501678517-il8gdqt5ak46nuh9r2oku23aeebg5f53.apps.googleusercontent.com"
                onSuccess={responseGoogle}
                onFailure={responseGoogle}
                cookiePolicy={'single_host_origin'}/>
            </div>
          </section>
          <div className="login-foot">
            {
              JoinLoign === '회원가입'
              ?
              (
                <>
                <span>이미 회원이신가요  ?</span>
                <div className="foot-link" onClick={(e)=>{
                e.preventDefault()
                setJoinLogin('로그인')
                }}>로그인</div>
                </>
              )
              :
              (
                <>
                <span>아직 회원이 아니신가요 ?</span>
                <div className="foot-link" onClick={(e)=>{
                e.preventDefault()
                setJoinLogin('회원가입')
                }}>회원가입</div>
                </>
              )
            }
          </div>
        </div>
      </div>
    </>
  )
}


export default LoginModal;

frontend/src/App.js

import React, {useState, useEffect} from 'react';
import Header from './components/Header';
import Navi from './components/Navi'
import LoginModal from './components/LoginModal';
import { Route } from 'react-router-dom';
import './App.css';

function App() {
  const [modal, setModal] = useState(false);
  const [user, setUser] = useState([])

  let [isAuthenticated, setisAuthenticated] = useState(localStorage.getItem('token') ? true : false)
  
  const userHasAuthenticated = (authenticated, username, token) => { 
    setisAuthenticated(authenticated)
    setUser(username)
    localStorage.setItem('token', token);
  }//회원가입이나 로그인이 성공했을 때 토큰을 저장

  const handleLogout = () => {
      setisAuthenticated(false)
      setUser("")
      localStorage.removeItem('token');
      setModal(false)
  }//로그아웃

  //회원가입이나 로그인이 성공했을 때 modal을 변경해 로그인 버튼을 없애고 글쓰기 버튼과 정보버튼을 나오게하는 setModal
  //useEffect의 두번째 인자는 모든 렌더링 후 두번째 인자가 변경될때에만 실행되라는 내용 
  useEffect(()=>{
    if(isAuthenticated){
      setModal(true)
    }
    else{
      setModal(false)
    }
  },[isAuthenticated])
  
  
  useEffect(() => {
    // 토큰(access token)이 이미 존재하는 상황이라면 서버에 GET /validate 요청하여 해당 access token이 유효한지 확인
    if (isAuthenticated) {
      // 현재 JWT 토큰 값이 타당한지 GET /validate 요청을 통해 확인하고
      // 상태 코드가 200이라면 현재 GET /user/current 요청을 통해 user정보를 받아옴
      fetch('http://localhost:8000/validate/', {
        headers: {
          Authorization: `JWT ${localStorage.getItem('token')}`
        }
      })
      .then(res => {
        fetch('http://localhost:8000/user/current/', {
          headers: {
            Authorization: `JWT ${localStorage.getItem('token')}`
          }
        })
        .then(res => res.json())
        .then(json => {
          // 현재 유저 정보 받아왔다면, 로그인 상태로 state 업데이트 하고
          if (json.username) {
            setUser(json.username);
          }else{
            //유저가 undefined라면 로그인버튼이 나오도록 modal을 false로 항상 맞춰줌
            setModal(false)
            setisAuthenticated(false)
          }
          // Refresh Token 발급 받아 token의 만료 시간 연장
          fetch('http://localhost:8000/refresh/', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              token: localStorage.getItem('token')
            })
          })
          .then(res => res.json())
          .then((json)=>{
            userHasAuthenticated(true, json.user.username, json.token);
          })
          .catch(error => {
            console.log(error);
          });;
        })
        .catch(error => {
          handleLogout();
          console.log(error)
        });
      })
      .catch(error => {
        handleLogout();
        console.log(error)
      });
    }
  },[isAuthenticated])

  return (
    <>
      <div className="App">
        <div className="auto-margin">
          <Route exact path="/">
            <Header modal={modal} handleLogout={handleLogout}/>
          </Route>

          <Route exact path="/">
            <Navi/>
          </Route>

          <Route exact path="/login">
            <LoginModal setModal={setModal} userHasAuthenticated={userHasAuthenticated}/>
          </Route>
      </div>
    </div>
    </>
  );
}

export default App;

앞서 LiginModal.js에서 설명한 내용을 제외하고 1장과 달라진 코드를 뜯어보자.

useEffect

사실 지금껏 내가올린 참조사이트를 읽고 이해했다면 useEffect말고는 다 설명했던 내용들이다.
그리고 2장의 시작하기 앞서 부분에서도 useEffect의 참조사이트를 링크걸어놨지만 이곳만큼 정확한 설명은 없다고 생각한다.
es6의 React Lifecycle은 componentdidmount, didupdate등이 있었는데 react의 Hook이 새롭게 출시되면서 didmount, didupdate를 합쳐놓은 새로운 Lifecycle hook이라고 보면된다.
공식홈페이지에서는 이렇게 설명하는데

useEffect가 하는 일은 무엇일까요? useEffect Hook을 이용하여 우리는 리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말합니다. 리액트는 우리가 넘긴 함수를 기억했다가(이 함수를 ‘effect’라고 부릅니다) DOM 업데이트를 수행한 이후에 불러낼 것입니다. 위의 경우에는 effect를 통해 문서 타이틀을 지정하지만, 이 외에도 데이터를 가져오거나 다른 명령형(imperative) API를 불러내는 일도 할 수 있습니다.

useEffect를 컴포넌트 안에서 불러내는 이유는 무엇일까요? useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 됩니다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있는 것입니다. Hook은 자바스크립트의 클로저를 이용하여 리액트에 한정된 API를 고안하는 것보다 자바스크립트가 이미 가지고 있는 방법을 이용하여 문제를 해결합니다.

useEffect는 렌더링 이후에 매번 수행되는 걸까요? 네, 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행됩니다.(나중에 effect를 필요에 맞게 수정하는 방법에 대해 다룰 것입니다.) 마운팅과 업데이트라는 방식으로 생각하는 대신 effect를 렌더링 이후에 발생하는 것으로 생각하는 것이 더 쉬울 것입니다. 리액트는 effect가 수행되는 시점에 이미 DOM이 업데이트되었음을 보장합니다.

이보다 더 설명을 잘할 자신이 없다 !

다시 코드 중 두번째 useEffect에 대해서 살펴보면

  useEffect(() => {
    // 토큰(access token)이 이미 존재하는 상황이라면 서버에 GET /validate 요청하여 해당 access token이 유효한지 확인
    if (isAuthenticated) {
      // 현재 JWT 토큰 값이 타당한지 GET /validate 요청을 통해 확인하고
      // 상태 코드가 200이라면 현재 GET /user/current 요청을 통해 user정보를 받아옴
      fetch('http://localhost:8000/validate/', {
        headers: {
          Authorization: `JWT ${localStorage.getItem('token')}`
        }
      })
      .then(res => {
        fetch('http://localhost:8000/user/current/', {
          headers: {
            Authorization: `JWT ${localStorage.getItem('token')}`
          }
        })
        .then(res => res.json())
        .then(json => {
          // 현재 유저 정보 받아왔다면, 로그인 상태로 state 업데이트 하고
          if (json.username) {
            setUser(json.username);
          }else{
            //유저가 undefined라면 로그인버튼이 나오도록 modal을 false로 항상 맞춰줌
            setModal(false)
            setisAuthenticated(false)
          }
          // Refresh Token 발급 받아 token의 만료 시간 연장
          fetch('http://localhost:8000/refresh/', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              token: localStorage.getItem('token')
            })
          })
          .then(res => res.json())
          .then((json)=>{
            userHasAuthenticated(true, json.user.username, json.token);
          })
          .catch(error => {
            console.log(error);
          });;
        })
        .catch(error => {
          handleLogout();
          console.log(error)
        });
      })
      .catch(error => {
        handleLogout();
        console.log(error)
      });
    }
  },[isAuthenticated])

LoginModal에서 로그인이 성공하면 hisory.back 이나 history.push를 통해 홈인 App.js로 돌아오면서 렌더링이 시작된다고 생각해보자. 그럼 렌더링이 끝난직후 useEffect의 수행이 시작되는데 이곳에.. fetch함수가 3개나 들어가있다. 당황하지말고 천천히 살펴보자.
먼저 'http://localhost:8000/validate/' 는 로그인 또는 회원가입에 성공해서 발급받은 토큰이 타당한 값인가 확인한다. 결과값이 200코드 즉 타당하다면 Response값인 res를 다시 자신의 유저정보(profile제외)를 GET 할 수 있는 'http://localhost:8000/user/current/' 요청하고 Response값인 내 정보를 state값에 담는 모습이다. 마지막으로 토큰의 유효기간이 끝나기전에 다시 로그인을 했다면 토큰의 만료기간을 연장시켜주는 'http://localhost:8000/refresh/'로 내토큰을 POST 요청으로 보내는 모습이다.
이렇게 간단하게 App.js의 코드를 살펴봤는데 Navi.js는 1장에서 변경된 사항이 없기때문에
마지막으로 Header부분에서 profile 기본이미지의 정보를 요청하고 state에 담는 모습을 살펴보고 Profile.js 작성 후 3장의 포스팅을 마무리하겠다 !

frontend/src/components/Header.js

import React, { useState, useEffect } from 'react';
import {Link} from 'react-router-dom';
import '../css/Header.css';

function Header(props){

  const [userprofile, setUserprofile] = useState(false)
  const [userPhoto, setUserPhoto] = useState()

  useEffect(()=>{
    fetch('http://localhost:8000/user/current/', {
      headers: {
        Authorization: `JWT ${localStorage.getItem('token')}`
      }
    })
    .then(res => res.json())
    .then(json => {
      // 현재 유저 정보 받아왔다면, 로그인 상태로 state 업데이트 하고
      if (json.id) {
        //유저정보를 받아왔으면 해당 user의 프로필을 받아온다.
    }fetch('http://localhost:8000/user/auth/profile/' + json.id + '/update/',{
            method : 'PATCH',
            headers: {
                Authorization: `JWT ${localStorage.getItem('token')}`
            },
        })
        .then((res)=>res.json())
        .then((userData)=> {
            setUserPhoto(userData.photo)
        })
        .catch(error => {
            console.log(error);
          });;
    }).catch(error => {
        console.log(error)
      });
},[userPhoto])

  return(
    <>
      <div className="header">
        <div className="header-nav">
          <div className="header-nav-links">
            <Link className="header-logo" to="/">Velog</Link>
            {
              props.modal === false
              ? <Link to="/login"><button className="header-btn">로그인</button></Link>
              : (
                <>
                <Link className="header-dashboard" to="/board"><button>새 글 작성</button></Link>
                <div className="user-container" onClick={()=>{setUserprofile(!userprofile)}}>
                  <img src={userPhoto} className="user-image" alt="/"></img>
                  <svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                    <path d="M7 10l5 5 5-5z"></path>
                  </svg>
                </div>
                {
                  userprofile === true
                  ?(
                    <div className="user-profile">
                      <div className="profile-menu">
                        <Link to="/mysite"><div className="menu">내가 쓴 글</div></Link>
                        <Link to="/profile"><div className="menu">내 정보</div></Link>
                        <Link onClick={props.handleLogout} to="/"><div className="menu">로그아웃</div></Link>
                      </div>
                    </div>
                  )
                  :null
                }
                </>
              )
            }
          </div>
        </div>
      </div>
    </>
  )
}

export default Header;

return()부분부터 살펴보자.

Header.js(props)는 modal이라는 값을 App.js로부터 바운딩 받아
로그인 성공여부에 따라 false, true가 setState되는 modal의 값인 props.modal가 false라면
로그인버튼이 나오고 로그인에 성공하여 props.modal이 true라면 새 글 작성버튼과 user의 프로필 이미지, profile페이지로 들어갈 수 있는 menu를 생성했다.
이 메뉴는 user profile이 포함되어있는 div태그에 onClick이벤트를 주어 클릭할 때 마다 userprofile라는 state값이 true, false로 변경되는 모습을 확인할 수 있다.

또한 backend의 Profile 모델에서 imagefiled에 default값으로 준 red.jpg를 성공적으로 useEffect를 통해 가져와 state값에 잘 넣어준 모습이다 !
마지막으로 App.js에서 바운딩받은 props에는 handleLogout도 있는데 이는 menu에서 로그아웃을 클릭시 실행되는 onClick의 이벤트에 props.handleLogout를 넣어줘 로그아웃을 가능하게 했다.

3장마치며

Profile.js의 실행결과를 보여주고 자세한 설명은 4장에서 이어가도록 하겠다.

이미지 업로드버튼클릭 후 이미지 선택시 이미지 미리보기가 가능하고 자기소개와 닉네임 소셜정보까지 수정이 가능하도록했다.
drf jwt단점으로 토큰의 삭제기능이 없기때문에 회원탈퇴기능을 아직 구현하지는 않았지만 Django를 통해 해당user의 정보를 email로 삭제요청한다면 superuser의 admin페이지 접속으로 user정보 삭제까지 4장에서 다뤄보려고하는데 본래 3장에서 다루기로한 Insomnia또한 4장으로 포스팅을 이어나가면서 여기까지 3장을 마치겠다.

profile
대현

1개의 댓글

comment-user-thumbnail
2021년 8월 2일

좋은 글 감사합니다!

답글 달기