블로그 API 클라이언트와 연결하기 -2-

Photo by Kostiantyn Li on Unsplash

회원가입 페이지

import "./register.scss"
import { Link } from "react-router-dom"
import { useState } from "react"
import axios from "axios";

export default function Register() {
  const [username,setUsername] = useState("");
  const [email,setEmail] = useState("");
  const [password,setPassword] = useState("");
  const [error,setError] =useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError(false);
    try {
      const res = await axios.post("http://localhost:5000/api/auth/register", {
        username,
        email,
        password,
      });
      console.log(res);
      res.data && window.location.replace("/login");
    } catch (err) {
      console.log(err);
      setError(true);
    }
  }
  return (
    <div className="register">
        <span className="register-title">회원가입</span>
        <form className="register-form" onSubmit={handleSubmit}>
            <label>User name</label>
            <input 
              type="text" 
              placeholder="이름을 입력해주세요 ..." 
              className="register-input" 
              onChange={e => setUsername(e.target.value)}
            />
            <label>Email</label>
            <input 
              type="text" 
              placeholder="이메일을 입력해주세요 ..." 
              className="register-input" 
              onChange={e => setEmail(e.target.value)}
            />
            <label>Password</label>
            <input 
              type="password" 
              placeholder="비밀번호를 입력해주세요 ..." 
              className="register-input"
              onChange={e => setPassword(e.target.value)}
            />
            <button type="submit" className="register-button">회원가입</button>
        </form>
        <span className="register-login">
            이미 회원이신가요? <Link to="/login" className="link">로그인</Link> 하러가기
        </span>
        {error && <span style={{color:"red"}}>이미 가입된 회원입니다.</span>}
    </div>
  )
}

아무정보도 없이 회원가입 요청을 보냈을때 응답

--->클라이언트에서 에러핸들링 (정보를 입력해달라는 메시지)

정보를 모두 입력했을때

같은정보로 한번 더 회원가입 했을때

---> 라우터에서 에러핸들링 (username, email 이 중복될때 조건 추가, 문맥에 맞는 err제공)

에러핸들링 try..catch로
console.log(err)

const [error,setError] = useState(false); 에러 상태 false로
회원가입 버튼을 눌렀을때 handleSubmit 실행 -> e.preventDefault()로 새로고침 방지 -> setError(false) 에러 false로 초기화 -> axios 요청 실행 (input value를 body에 전달) ->

  • 성공시 로그인 페이지로 이동
  • 실패시 catch문으로 -> setError(true)로 에러상태로 만듦 -> Error상태가 true일때 나타나는 에러메시지 출력

context API 사용하기

context를 위한 폴더 생성

//context.js
import { createContext, useReducer } from "react";
import Reducer from "./Reducer";

// 1.모든 렌더링 작업이 성공적으로 끝났을때 이 값을 업로드
const INITIAL_STATE = {
    user: null,
    inFetching: false,
    error: false,
}

// 2.이 값(INITIAL_STATE)을 가지고 콘텍스트 프로바이더 내보내기
export const Context = createContext(INITIAL_STATE);

// 3.콘텍스트에 접근하는 방법
export const ContextProvider = ({children}) => {
    //13. state dispatch하기, state,dispatch의 값은 Redcucer에서 가지고온다.
    //14. -> useReducer(Redcucer) -> Reducer.js에서 Reducer가져오기
    //15. 여기서 reducer가 사용하는 state는 INITIAL_STATE다. -> ,INITIAL_STATE)
    // 이제 Context를 이 Provider로 줄 수 있음
    const [state, dispatch] = useReducer(Reducer, INITIAL_STATE);

    //16. context.Provider의 props로 user,isFetching,error를 state에서 가져오고, 마지막으로dispatch한다.
    // ex 로그인 버튼을 누르면 이 정보들을 dispatch -> 서버에 응답에 따라 dispatch SUCCESFUL 이나 FAILURE를 가져옴
    //17. 이제 ContextProvider를 App.js나 index.js에서 사용가능
    // index.js에서 <App/>을 감싸게 되면 모든 곳에서 Context 사용가능 -> 18.index.js로
    return(
        <Context.Provider value={{
            user:state.user,
            isFetching:state.isFetching,
            error:state.error,
            dispatch,
        }}>{children}</Context.Provider>
    )
}

// 4.로그인 버튼을 눌렀을때 유저네임과 패스워드 두가지를 컨트롤해야한다 credential이 끝난 후
// 추가로 해야하는 일이 있다. 성공이냐 실패냐 일때 
// 성공시 INITIAL_STATE 의 프로퍼티들을 업데이트 user: abcd, email:asdfas...
// 실패시 (인가실패 몽고디비에러 등등) -> INITAL STATE의 error 값 바뀜
// 5. -> 이걸 Actions에서 컨트롤
//Actions.js
// 6.타입 -> 액션이름이;라 생각하면 됨
export const LoginStart = (userCredentials) => ({
    type: "LOGIN_START"
})

//7. payload -> user를 업데이트 하기 위한 프로퍼티
export const LoginSuccess = (user) => ({
    type: "LOGIN_SUCCESS",
    payload: user,
})

// 8.에러발생시 - 아무 페이로드도 인수로 집어넣지 않았다.
export const LoginFailure = () => ({
    type: "LOGIN_FAILURE"
})

// 9.이제 이 액션들을 가지고 state를 다룰 수 있는게 reducer임
const Reducer = (state, action) => {
    //10.switch로 제어
    switch(action.type){
        case "LOGIN_START":
            return {
                user:null,
                insFetching:true,
                error:false,
            };
            //10.payload에 있는 user를 user값으로 바꿔준다.
            //11.fetch는 끝났으니 false로 (작업을 여기서 끝내야되니까)
        case "LOGIN_SUCCESS":
            return {
                user:action.payload,
                isFetching: false,
                error:false,
            }
            // 11. error 발생상황이니 error 는 true로
            // 작업은 끝난상태니 isFetching은 여전히 false
        case "LOGIN_FAILURE":
            return {
                user:null,
                isFetching: false,
                error:true,
            }
        default:
            return state;
    }
}

//12. 이제 Action과 Reducer를 사용하려면 이것들을 dispatch하면 된다. ->Context로
export default Reducer;
//index.js
import React from 'react';
// import ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client'
import App from './App';
import { ContextProvider } from './context/Context';


// ReactDOM.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>,
//   document.getElementById('root')
// );

const rootElement = document.getElementById('root');

// 18. ContextProvider로 context를 줄 컴포넌트 감싸기 -> App을 감싸게 되면 모든곳에서 사용가능
const root = ReactDOMClient.createRoot(rootElement);
root.render(
  <React.StrictMode>
    <ContextProvider>
      <App callback={() => console.log("rendered")}/>
    </ContextProvider>
  </React.StrictMode>
);
//login.jsx
import "./login.scss"
import { Link } from "react-router-dom"
import axios from "axios";
import { useContext, useRef } from "react";
import { Context } from "../../context/Context";

export default function Login() {

  const userRef = useRef();
  const passwordRef = useRef();
  const { user, dispatch, isFecthing } = useContext(Context);

  const handleSubmit = async (e) => {
    e.preventDefault();
    dispatch({type:"LOGIN_START"});
    try {
      const res = await axios.post("http://localhost:5000/api/auth/login", {
        username: userRef.current.value,
        password: passwordRef.current.value,
      })
      dispatch({ type:"LOGIN_SUCCESS", payload: res.data });
    } catch (err) {
      dispatch({ type:"LOGIN_FAILURE" })
    }
  }

  console.log(user);
  return (
    <div className="login">
        <span className="login-title">환영합니다.</span>
        <form className="login-form" onSubmit={handleSubmit}>
            <label>username</label>
            <input ref={userRef} type="text" placeholder="이름을 입력해주세요 ..." className="login-input" />
            <label>Password</label>
            <input ref={passwordRef} type="password" placeholder="비밀번호를 입력해주세요 ..." className="login-input"/>
            <button type="submit" className="login-button">로그인</button>
        </form>
        <button className="login-register-button">
            <Link to="/register" className="link">회원가입</Link>
        </button>
    </div>
  )
}

React - useRef
React - Ref 와 DOM
이번엔 인풋값을 useRef() 이용하여 가져온다.

  • userRef.current.value
    useRef의 current에 담고있는 값들을 사용할 수 있다.

  • input ref={userRef} const userRef = useRef();
    useRef()의 대상으로 ref를 이용해 DOM 엘레멘트를 불러올 수 있다.

  • context 가져오기
    const {불러올콘텍스트state1,state2... , dispatch} = useContext(불러올콘텍스트);

  • dispatch({type:"LOGIN_START"});
    로그인을 시작하기 위해 콘텍스트 값을 로그인을 시작한는 액션 이름인 "LOGIN_START"를 dispatch로 불러온다.

  • dispatch({ type:"LOGIN_SUCCESS", payload: res.data });
    로그인에 성공했을경우 "LOGIN_SUCCESS" 액션을 불러오고, res.datapayload에 할당한다.

  • dispatch({ type:"LOGIN_FAILURE" })
    에러발생시 "LOGIN_FAILURE" 로 값을 바꾼다.

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client 라는 에러가 발생했다.

요청은 한번에 하나만 할 수 있는데 두번이상 들어가면 나오는 에러라고 한다.
그런데 포스트맨에서는 또 잘만 된다...

리액트 라우터 돔 문제인 것 같아서 App에 있는 라우터 구조를 바꿔봐도 해결이 안됬다.

결국 라우터안의 조건문을 ifelse if 조건문으로 바꿔서 해결했다.

//LOGIN
router.post("/login", async (req, res) => {
    try {
        console.log(req,"req")
      	//바꾸기 전 코드
        //const user = await User.findOne({ username: req.body.username });
    	//!user && res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 아이디가 틀림)");

    	//const validated = await bcrypt.compare(req.body.password, user.password);
    	//!validated && res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 비밀번호가 틀림)");
        //const { password, ...나머지정보들 } = user._doc;
        //res.status(200).json(나머지정보들);
        //console.log("요청성공")
      
      // 에러 해결
      	const user = await User.findOne({ username: req.body.username })
        const validated = await bcrypt.compare(req.body.password, user.password);
      	if (!user) {
            console.log("아이디문제")
            res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 아이디가 틀림)");
        } else if(!validated) {
            console.log("비번문제");
            return res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 비밀번호가 틀림)");
        } else {
            const { password, ...나머지정보들 } = user._doc;
            res.status(200).json(나머지정보들);
            console.log("요청성공")
        }
    } catch (err) {
        console.log("에러발생")
        res.status(500).json(err);
    }
})

서버에서 만들어놓은 에러메세지 출력하기

백엔드에서 만들었던 에러메세지를 출력하려고 res로 접근하려 했더니 정의되지 않았다고 뜬다.

catch가 받는 err객체를 console.dir(err)로 살펴보니 api를 만들때 만들어놓았던 메시지가 data에 들어가있다.
err객체로 접근해서 err.response.data를 출력하면 서버(auth.js)에서 지정한 메시지를 출력할 수 있었다.

  • 로그인 성공시 응답

좋은 웹페이지 즐겨찾기