티스토리 뷰

개요

Frontend next.js, Redux, Redux-saga를 사용하였고

Backend는 mongoDB, express 를 이용해 구현했다.

 

왜 Next.js 인가?

기존 CRA(create react application) 은 클라이언트 사이드 렌더링방식으로 초기 렌더링시에 HTML을 받고 리소스를 받아 렌더링 하기에 맨처음 로딩까지의 시간이 SSR보다 느리다. 또한 빈 HTML에 JS로 VIEW를 구성하기에 SEO부분에서도 검색엔진이 데이터를 수집하는데에 문제가 있다. 때문에 CRA에서 SSR 구성을 어찌어찌 해야하는데, 반면 Next.js 는 기본적으로 서버사이드 렌더링을 제공하며, 코드스플리팅을 자동화 해주고(여기서 코드스플리팅이란 하나의 작은 번들을 잘개 쪼개어 사용자가 필요한 코드만 로드하여 성능을 향상시켜주는 것을 말한다) SSR이기에 SEO에서도 최적화되어있고 초기 로딩속도또한 빠르다고 한다.

 

https://velog.io/@rjs1197/SSR%EA%B3%BC-CSR%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

SPA에서의 SSR과 CSR

이 글은 SPA(Single Page Application)에서의 SSR과 CSR을 비교하기 위해 작성되었다(편의를 위해 반말 사용). - 최근 수정: 2019. 09. 22 초기 View 로딩 속도 먼저 전통적인 SSR과 CSR의 가장 큰 차이 중 하나인 '�

velog.io

 

어쨌든 만들껀?

간단하게 매일 주제를 던져주고 그에 맞는 사진을 사용자가

업로드하는 갤러리형 사이트를 만든다.

1. Front

 

프론트에서 미리 회원가입 폼을 작성해 놓았다.

이는 리덕스와 리덕스 사가로 더미데이터를 넣어

가상으로 회원가입 후 자동 로그인되는 방식으로 구현해 놓았다.

 

 const signUp = (e) => { //onSubmit 시 동작하는 함수
    e.preventDefault();
    if (userPassword !== userPasswordCheck) { //미리 password 의 일치여부를 파악한다.
      return setPasswordCheckError(true);
    }
    return dispatch({
      type: SIGN_UP_REQUEST,
      data: {
        email: userEmail,
        password: userPassword,
        name: userName,
      },
    });
  };

 

SignUp 페이지에서 폼데이터가 onSubmit 되는 순간

리듀서 중 SIGN_UP_REQUEST 가 dispatch 되어 지고 action data 로 폼 안의 email, password, userName 이 전달된다.

 

function signUpAPI(signUpData) {
  return axios.post('http://localhost:5000/signup', signUpData, { //signUpAPI로 요청을 보낸다
    withCredentials: true,//cors 쿠키 교환을 위한것. back에서 cors 를 사용해 credentials: true 한번더 처리해줘야한다
  });
}

function* signUp(action) {
  try {
    const result = yield call(signUpAPI, action.data); //signUpAPI 를 호출하고 돌아오는 데이터도 받는다.
    yield put({
      type: SIGN_UP_SUCCESS,
      data: result.data,
    });
  } catch (e) {
    console.log(e);
    yield put({
      type: SIGN_UP_FAILURE,
    });
  }
}

function* watchSignUp() {
  yield takeEvery(SIGN_UP_REQUEST, signUp); //리듀서 감지
}

그럼 동시에 redux-saga 에서 비동기로 

SIGN_UP_REQUEST 를 감지하고 넘어온 action data 를 backend 서버로 전달한다.

 

1. Back

import mongoose from 'mongoose';
import passportLocalMongoose from 'passport-local-mongoose';
const UserSchema = new mongoose.Schema({
  name: String,
  writer: String,
  email: String,
  userInfo: String,
  likeList: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Photo',
    },
  ],
});

UserSchema.plugin(passportLocalMongoose, { usernameField: 'email' });

const model = mongoose.model('User', UserSchema);

export default model;

User 모델을 만들어 놓았다. likeList는 좋아요를 구성하기 위함이다.

일전에 로그인구현하는 예제에서 사용한 그대로 썼다.

편하게 하기위하여 passport-local-mongoose 사용도 잊지 않았다.

 

globalRouter.post(routes.signup, signup, login); //routes.signup = /signup

회원가입 후 자동으로 로그인되는 순서다.

export const signup = async (req, res, next) => {
  const { email, name, password } = req.body;
  try {
    const user = await User({
      name,
      email,
      userInfo: '',
      writer: '',
    });
    await User.register(user, password);
    next();
  } catch (e) {
    console.log(e);
    res.send('가입실패');
  }
};

아까 front 에서 데이터를 넘겨주면 back 에서는 그 데이터를

req.body 안에서 확인할 수 있다.

가입방법은 예전에 쓴 그 글을 참고하면 된다.

 

2020/03/23 - [Study/script.js] - [Node.js] 회원가입 구현(로그인,로그아웃,회원가입,passport, Mongo Store)

 

[Node.js] 회원가입 구현(로그인,로그아웃,회원가입,passport, Mongo Store)

Passport 로컬 로그인 뿐만아니라 소셜 로그인에서까지 쉽게 인증이 가능한 미들웨어이다. http://www.passportjs.org/ Passport.js Simple, unobtrusive authentication for Node.js www.passportjs.org ** mongo..

andwinter.tistory.com

근데 왜 userInfo와 writer 을 남겨놓았냐면

회원가입이 완료되면 모달창이 뜨고 그 모달에서 작가명과 자기소개를

작성할 수 있는 기능을 원했기 때문이다.

쓸대없어 보이겠지만 그냥 그걸 원했다.

그리고 이 모달창은 이후에 editProfile 에서도 동일하게 이용할것이다.

 

아무튼 회원가입을 누르게되면 로그인으로 가야하니 로그인까지 구현한다.

export const login = (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      console.log(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info);
    }
    req.login(user, (loginErr) => {
      if (loginErr) {
        return next(loginErr);
      }
      const userData = Object.assign({}, user.toJSON());
      delete userData.password; //객체안의 password 정보를 지운다.
      return res.json(userData);
    });
  })(req, res, next);
};

예전에 작성했던 회원가입글과 다른점이 이거 하나다.

여기서는 custom callback 를 이용했다는점.

이는 프론트와 백이 나누어져있고 프론트는 next.js 로 이루어져있기때문에

redirect 가 필요하지 않았다. 해서 custom callback 를 이용해서

로그인이 성공하면 해당 유저 데이터를 프론트서버로 다시 돌려보내주게

만들었다.

 

이렇게 signUp 이 완료되고 next() 로 login 으로와서 로그인까지 된 상태가 되면

res.json(userData) 가 실행되어 프론트로 로그인된 사용자 정보가 넘어가게 된다.

 

2. 다시 Front

프론트에선 그 json 데이터를 

try {
    const result = yield call(signUpAPI, action.data);
    yield put({
      type: SIGN_UP_SUCCESS,
      data: result.data,
    });
  }

아까 try catch 문의 try 에서 result 로 받게 되고

그 데이터를 put(dispatch 와 동일) 을통해 SIGN_UP_SUCCESS 를 호출하여 데이터를 넘긴다.

    case SIGN_UP_SUCCESS: {
      return {
        ...state,
        isUserLoadding: false,
        me: action.data,
        editing: true,
      };

리덕스에서는 미리 설정해놓은 action에 따라서 그 유저데이터를 me 라는 state 에 저장하게된다.

또, editing 이라는 state 의 값을 true 로 바꾸어 모달창을 띄운다.

 

그럼 이러한 모달창이 뜨게된다.

여기서 아까 말했듯, 이름과 별개로 작가명, 작가소개를 작성할 수 있다.

여기서 작성되는 데이터는 마찬가지로 onSubmit 되어지면

 const handleSubmit = (e) => {
    e.preventDefault();
    const writerData = {
      userInfo: info.userInfo,
      writer: info.name,
    };
    dispatch({
      type: EDITING_PROFILE_REQUEST,
      data: { me, writerData },
    });
  };

이 함수가 실행된다.

회원가입할때와 비슷한 구조로 dispatch 되고

function editingAPI(userID) {
  return axios.post('http://localhost:5000/editing', userID, {
    withCredentials: true,
  });
}

function* editing(action) {
  try {
    const result = yield call(editingAPI, action.data);
    const userData = JSON.parse(result.config.data);
    const { me, writerData } = userData;
    yield put({
      type: EDITING_PROFILE_SUCCESS,
      data: { ...me, ...writerData },
    });
  } catch (e) {
    console.log(e);
    yield put({
      type: EDITING_PROFILE_FAILURE,
    });
  }
}

function* watchEditing() {
  yield takeEvery(EDITING_PROFILE_REQUEST, editing);
}

redux-saga 에서는 이러한 과정으로 비동기요청을 한다.

 

3. 다시 back

export const editing = async (req, res) => {
  const {
    me: { _id, email, name },
    writerData: { userInfo, writer },
  } = req.body;
  try {
    await User.findByIdAndUpdate(_id, {
      name,
      email,
      userInfo,
      writer,
    });
    const user = req.user;
    return res.json(user);
  } catch (e) {
    console.log(e);
    res.send('프로필 업데이트 실패');
  }
};

back 에서는 그 데이터를 받아서 mongoose 의 findByIdAndUpdate 를 통하여

아까 비어있던 userInfo 와 writer 을 채우게 된다.

그리고 다시 프론트로 정보를 넘겨주고

 

4. 다시다시 Front

프론트에서는 그 정보를 받아서 사용자 정보를 업데이트 한다.

    case EDITING_PROFILE_SUCCESS: {
      return {
        ...state,
        isLoggedIn: true,
        editing: false,
        isUserLoadding: false,
        me: action.data,
      };
    }

이렇게 회원가입은 끝나게 된다.

 

이미 위에 back서버의 로그인은 만들어져있기때문에 그 api를 이용하는

프론트 서버의 무언가를 만들어주기만 하면된다.

 

요런 로그인창을 이용할 것이고,

onsubmit 되어지면 회원가입과 똑같은 패턴으로 back 서버의 loginapi 를 이용하여 로그인시키면 된다.

대신 로그인으로 넘길때에는 email과 password 라는 이름으로 넘겨야 한다.


5. 여기서 문제는? 로그인이 풀리는것!

여기까진 반쪽짜리 회원가입, 로그인이다. 왜냐하면

새로고침하면 로그인이 풀려버리기 때문이다.

이는 프론트에서 만들어진 쿠키를 백서버로 서로 교환하지 못하기 때문인데,

위에서 적어줬던 axios 의

withCredentials: true,

이 옵션과

app.use(
  cors({
    origin: true,
    credentials: true,
  }),
);

백의 app.js 에서 cors 를 이용하여 이 옵션들을 설정해주면 쿠키가 올바르게

생성되는걸 볼 수 있다.

 

6. 하지만 그래도 로그인이 풀리는걸?

하지만 쿠키생성만으로는 로그인이 풀리는걸 막을순 없다.

쿠키는 단지 사용자의 정보 일부분을 가지고있을뿐이다.

때문에 이 쿠키를 back으로 보내고 express session과 passport의 deserializeUser를 통해 이 쿠키를 해석하고 req.user에 로그인된 유저의 정보가 담기면 그 정보를 프론트로 다시 보내주는 작업을 해야 로그인이 풀려버리는걸 막을 수 있다.

(아 맞아 미리 세션은 다 설정해놓았다. 설정하는 방법은 저번 글(위에글) 참고 )

즉 모든 페이지에서 쿠키를 이용해서 로그인된 사용자 정보를 매번 불러와주는것이 필요한 것이다.

 

이를위해 모든 페이지의 상위에있는 Layout 에다가 작업을 해줄 것이다.

 

useEffect 를 사용하여 페이지가 로드되는 맨처음에(componentDidMount() 와 같음)

LOAD_USER_REQUEST 를 호출하는 작업을 해줄것이다.

  useEffect(() => {
    dispatch({
      type: LOAD_USER_REQUEST,
    });
  }, []);

layout 에 위와같은 코드를 적어주었고, saga에서는

function loadUserAPI() {
  return axios.get('http://localhost:5000/load', {
    withCredentials: true,
  });
}
function* loadUser() {
  try {
    const result = yield call(loadUserAPI);
    yield put({
      type: LOAD_USER_SUCCESS,
      data: result.data,
    });
  } catch (e) {
    console.log(e);
    yield put({
      type: LOAD_USER_FAILURE,
    });
  }
}
function* watchloadUser() {
  yield takeEvery(LOAD_USER_REQUEST, loadUser);
}

이렇게 get 요청을 보낼것이다.

이미 사전작업으로 인해 쿠키가 존재하기때문에 프론트에서 뭔가를 넘겨줄 필요가 없고 단지 로그인된 유저의 정보만을 불러오면 되기 때문이다.

 

7. 마지막으로 Back

백에서는

export const loadUser = (req, res) => {
  if (!req.user) {
    return res.status(401).send('로그인 되어있지 않음.');
  }
  return res.json(req.user);
};

이렇게 req.user 를 넘겨주기만 하면 끝이다. 간단하쥬?

 

8. 진짜 마지막으로 로그아웃

로그아웃은 정말 간단하다.

버튼하나 만들고

  const handleLogout = () => {
    dispatch({
      type: LOG_OUT_REQUEST,
    });
  };
// 로그아웃
function logOutAPI() {
  return axios.post(
    'http://localhost:5000/logout',
    {},
    {
      withCredentials: true,
    },
  );
}
function* logOut() {
  try {
    yield call(logOutAPI);
    yield put({
      type: LOG_OUT_SUCCESS,
    });
  } catch (e) {
    console.log(e);
    yield put({
      type: LOG_OUT_FAILURE,
    });
  }
}
function* watchLogOut() {
  yield takeEvery(LOG_OUT_REQUEST, logOut);
}

이렇게 해주고(매번똑같다)

 

백에서는

export const logout = (req, res) => {
  req.logout();
  req.session.destroy(() => {
    res.clearCookie('connect.sid');
    res.send('로그아웃 성공');
  });
};

요렇게 세션을 파괴하고 쿠키를 지워주면 끝이다!

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함