React localhost가 다른 프론트서버와 백엔드 서버의 연결 중 생긴 오류 해결

개발공부·2022년 11월 7일
0

React 공부하기

목록 보기
4/14

* 문제

  • 프론트 서버(localhost:3060)과 백엔드 서버(localhost:3065)를 연결하는 과정에서 오류 생김
  • 가장 많이 본 오류 : cannot read properties of undefined (reading 'data')
  • 프론트는 redux, redux-saga를, 백엔드는 mysql과 sequelize 사용
  • 회원가입은 정상적으로 작동되나 로그인에서 문제 생김

* 해결

  • 숫자 1을 콘솔에 찍어보니 loginErr쪽으로 나왔음
    (saga쪽에 문제가 있었던 것으로 생각해 reducer와 saga를 계속 확인함)
    ▷ 결론적으론 서버의 model 부분을 정확히 입력하지 않았음(오타가 있었습니다;)
  • app.js에서 morgan을 불러오지 않음(morgan : node.js의 로그 관리 라이브러리)
  • passport/local.js에서 User.findOne을 User.fineOne으로 오타냄;
  • routes/user.js에서 include에 해당하는 부분 중 model: User 부분이 정확히 입력되지 않음
    (동시에 model에서 User.js를 확인하니 db.User.belongsToMany(Follow 부분)가 db.User로 연결되어야 했는데 db.Post로 한 부분도 존재)
    ▷ (참고 : https://www.inflearn.com/questions/544828)
    - model(특히 User) 자체에서 오류가 난 부분들이 있어 data를 프론트에서 전달해도 서버에서 받지 못한 것으로 생각됨

* app.js

  • cors 설치 (서버에서 서버로 요청 보내는 것이 아닌 서로 다른 localhost를 가진 도메인 서버로 요청 보냈을 때 해결책)
  • passport, passort-local 설치
    (passport : 여러 경로 로그인 관리(Strategy 사용))
    (passport-local : email&비밀번호나 id&비밀번호로 로그인 가능)
  • 로그인 시 보안을 위해 쿠키, session, bcypt 사용됨
  • 보안을 위해(mysql 비밀번호와 쿠키 시크릿) dotenv 사용(.env 파일에 저장 후 .gitignore에 .env 제외하기)
const express = require("express");
const cors = require("cors");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const passport = require("passport");
const morgan = require("morgan");
const dotenv = require("dotenv");

const postRouter = require("./routes/post");
const userRouter = require("./routes/user");
const db = require("./models");
const passportConfig = require("./passport");

dotenv.config();

const app = express();

db.sequelize
  .sync()
  .then(() => {
    console.log("DB 연결 성공");
  })
  .catch(console.error);

passportConfig();

app.use(morgan("dev"));
app.use(
  cors({
    origin: "*",
    credentials: false,
  })
); //모든 요청에 설정을 넣어줌
//req.body 사용하기 위함
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
  session({
    saveUninitialized: false,
    resave: false,
    secret: process.env.COOKIE_SECRET,
  })
);
app.use(passport.initialize());
app.use(passport.session());

app.get("/", (req, res) => {
  res.send("hello express");
});

app.get("/", (req, res) => {
  res.send("hello");
});

app.get("/posts", (req, res) => {
  res.json([
    { id: 1, content: "hello" },
    { id: 2, content: "hello 2" },
    { id: 3, content: "hello 3" },
  ]);
});

app.use("/post", postRouter);
app.use("/user", userRouter);

app.listen(3065, () => {
  console.log("서버 실행 중");
});

[back 구조]

《 models(폴더) 》
┗ post.js
┗ user.js
《 passport(폴더) 》
┗ index.js
┗ local.js
《 routes(폴더) 》
┗ user.js

* models/user.js

▶ db.User.belongsToMany에서 값 미입력 및 오타 있었음

module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define(
    "User",
    {
      email: {
        type: DataTypes.STRING(30),
        allowNull: false, //필수
        unique: true, //고유한 값
      },
      nickname: {
        type: DataTypes.STRING(30),
        allowNull: false, //필수
      },
      password: {
        type: DataTypes.STRING(100),
        allowNull: false, //필수
      },
    },
    {
      charset: "utf8",
      collate: "utf8_general_ci", //한글 저장
    }
  );
  User.associate = (db) => {
    db.User.hasMany(db.Post);
    db.User.hasMany(db.Comment);
    db.User.belongsToMany(db.Post, { through: "Like", as: "Liked" });
    db.User.belongsToMany(db.User, {
      through: "Follow",
      as: "Followers",
      foreignKey: "FollowingId",
    });
    db.User.belongsToMany(db.User, {
      through: "Follow",
      as: "Followings",
      foreignKey: "FollowerId",
    });
  };
  return User;
};

* passport/index.js

const passport = require("passport");
const local = require("./local");
const { User } = require("../models");
module.exports = () => {
  passport.serializeUser((user, done) => {
    done(null, user.id);
  });
  passport.deserializeUser(async (id, done) => {
    try {
      const user = await User.findOne({ where: { id } });
      done(null, user);
    } catch (error) {
      console.error(error);
      done(error);
    }
  });
  local();
};

* passport/local.js

const passport = require("passport");
const { Strategy: LocalStrategy } = require("passport-local");
const bcrypt = require("bcrypt");
const { User } = require("../models");
module.exports = () => {
  passport.use(
    new LocalStrategy(
      {
        usernameField: "email",
        passwordField: "password",
      },
      async (email, password, done) => {
        try {
          const user = await User.findOne({
            where: { email },
          });
          if (!user) {
            done(null, false, { reason: "존재하지 않는 사용자입니다!" });
          }
          const result = await bcrypt.compare(password, user.password);
          if (result) {
            return done(null, user); //성공에 사용자 정보 넘김
          }
          return done(null, false, { resaon: "비밀번호가 틀렸습니다." });
        } catch (error) {
          console.error(error);
          return done(error);
        }
      }
    )
  );
};

* routes/user.js

const express = require("express");
const bcrypt = require("bcrypt");
const passport = require("passport");
const { User, Post } = require("../models");
const { isLoggedIn, isNotLoggedIn } = require("./middlewares");
const router = express.Router();

//로그인
router.post("/login", isNotLoggedIn, (req, res, next) => {
  passport.authenticate("local", (err, user, info) => {
    if (err) {
      console.error(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info.reason);
    }
    return req.login(user, async (loginErr) => {
      console.log("loginErr", loginErr);
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }
      const fullUserWithoutPassword = await User.findOne({
        where: { id: user.id },
        attributes: {
          exclude: ["password"],
        },
        include: [
          {
            model: Post,
            attributes: ["id"],
          },
          {
            model: User,
            as: "Followings",
            attributes: ["id"],
          },
          {
            model: User,
            as: "Followers",
            attributes: ["id"],
          },
        ],
      });
      return res.status(200).json(fullUserWithoutPassword);
    });
  })(req, res, next);
});

//로그아웃
router.post("/logout", isLoggedIn, (req, res) => {
  req.logout();
  req.session.destroy();
  res.send("ok");
});

//회원가입
router.post("/", isNotLoggedIn, async (req, res, next) => {
  try {
    const exUser = await User.findOne({
      where: {
        email: req.body.email,
      },
    });
    if (exUser) {
      //반드시 return 붙여야 함
      return res.status(403).send("이미 사용 중인 아이디 입니다.");
    }
    const hashedPassword = await bcrypt.hash(req.body.password, 12);
    await User.create({
      email: req.body.email,
      nickname: req.body.nickname,
      password: hashedPassword,
    });
    res.status(201).send("ok");
  } catch (error) {
    console.error(error);
    next(error);
  }
});

module.exports = router;

[front 구조]

《 components(폴더) 》
┗ LoginForm.js(로그인)
┗ UserProfile.js(로그인 후 나오는 회원정보)
《 pages(폴더) 》
┗ index.js(로그인)
┗ signup.js(회원가입)
《 reducer(폴더) 》
┗ index.js
┗ post.js
┗ user.js
《 store(폴더) 》
《 《 sagas(폴더) 》 》
┗ index.js
┗ post.js
┗ user.js

* reducer/user.js

export const initialState = {
 logInLoading: false, //로그인 시도 중
 logInDone: false,
 logInError: null,
 logOutLoading: false, //로그아웃 시도 중
 logOutDone: false,
 logOutError: null,
 signUpLoading: false, //회원가입 시도 중
 signUpDone: false,
 signUpError: false,
 }

export const LOG_IN_REQUEST = "LOG_IN_REQUEST";
export const LOG_IN_SUCCESS = "LOG_IN_SUCCESS";
export const LOG_IN_FAILURE = "LOG_IN_FAILURE";

export const LOG_OUT_REQUEST = "LOG_OUT_REQUEST";
export const LOG_OUT_SUCCESS = "LOG_OUT_SUCCESS";
export const LOG_OUT_FAILURE = "LOG_OUT_FAILURE";

export const SIGN_UP_REQUEST = "SIGN_UP_REQUEST";
export const SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS";
export const SIGN_UP_FAILURE = "SIGN_UP_FAILURE";

export const loginRequestAction = (data) => ({
 type: LOG_IN_REQUEST,
 data,
});

export const logoutRequestAction = () => ({
 type: LOG_OUT_REQUEST,
});

const reducer = (state = initialState, action) => {
 return produce(state, (draft) => {
   switch (action.type) {
     case LOG_IN_REQUEST:
       console.log("reducer logIn");
       draft.logInLoading = true;
       draft.logInError = null;
       draft.logInDone = false;
       break;
     case LOG_IN_SUCCESS: {
       draft.logInLoading = false;
       draft.logInDone = true;
       draft.me = action.data;
       break;
     }
     case LOG_IN_FAILURE: {
       draft.logInLoading = false;
       draft.logInError = action.error;
       break;
     }
     case LOG_OUT_REQUEST:
       draft.logOutLoading = true;
       draft.logOutDone = false;
       draft.logOutError = null;
       break;
     case LOG_OUT_SUCCESS:
       draft.logOutLoading = false;
       draft.logOutDone = true;
       draft.me = null;
       break;
     case LOG_OUT_FAILURE:
       draft.logOutLoading = false;
       draft.logOutError = action.error;
       break;
     case LOG_OUT_REQUEST:
       draft.logOutLoading = true;
       draft.logOutDone = false;
       draft.logOutError = null;
       break;
     case LOG_OUT_SUCCESS:
       draft.logOutLoading = false;
       draft.logOutDone = true;
       draft.me = null;
       break;
     case LOG_OUT_FAILURE:
       draft.logOutLoading = false;
       draft.logOutError = action.error;
       break;
     // signup
     case SIGN_UP_REQUEST:
       draft.signUpLoading = true;
       draft.signUpDone = false;
       draft.signUpError = null;
       break;
     case SIGN_UP_SUCCESS:
       draft.signUpLoading = false;
       draft.signUpDone = true;
       draft.me = null;
       break;
     case SIGN_UP_FAILURE:
       draft.signUpLoading = false;
       draft.signUpError = action.error;
       break;
   }
 });
};

export default reducer;

* store/sagas/index.js

▶ axios.defaults.baseURL 사용하기 위해서는 front에서도 axios 설치할 것
▶ baseURL을 한 번 만들면 sagas/user.js에서 자동으로 URL 주소가 붙어서 적용됨

import { all, fork } from "redux-saga/effects";
import axios from "axios";
import postSaga from "./post";
import userSaga from "./user";

axios.defaults.baseURL = "http://localhost:3065";

export default function* rootSaga() {
  yield all([fork(postSaga), fork(userSaga)]);
}

* store/sagas/user.js

import { all, takeLatest, delay, put, fork, call } from "redux-saga/effects";
import axios from "axios";
import {
  LOG_IN_REQUEST,
  LOG_IN_SUCCESS,
  LOG_IN_FAILURE,
  LOG_OUT_REQUEST,
  LOG_OUT_SUCCESS,
  LOG_OUT_FAILURE,
  SIGN_UP_REQUEST,
  SIGN_UP_SUCCESS,
  SIGN_UP_FAILURE,
} from "../../reducers/user";

function logInAPI(data) {
  return axios.post("/user/login", data);
}

function* logIn(action) {
  try {
    const result = yield call(logInAPI, action.data);
    yield put({
      type: LOG_IN_SUCCESS,
      data: result.data,
    });
  } catch (err) {
    console.log(err);
    yield put({
      type: LOG_IN_FAILURE,
      error: err.response.data,
    });
  }
}

function logOutAPI() {
  return axios.post("/user/logout");
}

function* logOut() {
  try {
    // const result = yield call(logOutAPI)
    yield delay(1000);
    yield put({
      type: LOG_OUT_SUCCESS,
    });
  } catch (err) {
    yield put({
      type: LOG_OUT_FAILURE,
      error: err.response.data,
    });
  }
}

function signUpAPI(data) {
  return axios.post("/user", data);
}

function* signUp(action) {
  try {
    console.log("action", action);
    console.log("action.data", action.data);
    const result = yield call(signUpAPI, action.data);
    console.log(result);
    yield put({
      type: SIGN_UP_SUCCESS,
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: SIGN_UP_FAILURE,
      error: err.response.data,
    });
  }
}

function* watchLogin() {
  yield takeLatest(LOG_IN_REQUEST, logIn);
}

function* watchLogOut() {
  yield takeLatest(LOG_OUT_REQUEST, logOut);
}

function* watchSignUp() {
  yield takeLatest(SIGN_UP_REQUEST, signUp);
}

export default function* userSaga() {
  yield all([
    fork(watchLogin),
    fork(watchLogOut),
    fork(watchSignUp),
  ]);
}

* 전체 코드 확인 가능한 곳

https://github.com/ZeroCho/react-nodebird/tree/master/ch5

profile
개발 블로그, 티스토리(https://ba-gotocode131.tistory.com/)로 갈아탐

0개의 댓글