회원가입 및 로그인 구현 #1

chu·2021년 4월 8일
14

이번 시간에는 회원가입과 로그인을 구현을 진행한 작업에 대해서 정리를 하려고 한다.
React Next.js typescript 로 프론트를 작업하였고,
express MySQL sequelize passport로 백 부분을 작업했다.

백 서버와 DB를 구축해서 실제 저장된 계정으로 로그인을 하는 과정입니다.

로그아웃 시 비밀번호 일치하지 않습니다. 수정해야겠네요..😂 수정 끝~

깃헙에서 코드 확인 가능합니다.


투두앱을 제외한 프론트와 백을 구축해서 어떤 개인 실습을 해볼까 하다가 어느 서비스에나 존재하는
기능인 회원가입, 로그인을 구현하기로 하였다.

주의 : 타입스크립트라고 했지만 거의 자바스크립트에 가까운 코드입니다...😭

생각보다 타입스크립트... 후.. 하다보니 그냥 자바스크립트로 작업이 되었다...


Next.js

Next.js 로 라우팅과 서버사이드렌더링(SSR)을 사용하였고, SSR은 이미 흔하게 사용되는 부분이기 때문에 특별하게 설명은 하지 않겠습니다.

그럼 여기에 왜 썼을까?

로그인이라 하면 로그인 후 어느 페이지를 가도 항상 로그인이 되어 있어야한다.
이 부분은 쿠키와 세션으로 처리는 되지만, 중요한건 고객의 사용경험이다.
리액트 기본적으로 CSR로 할 경우 페이지 업로드 후 필요한 로그인 데이터를 받아와서
로그인을 유지 시킨다. 그 사이에 깜빡거림(?) 같은 사용경험에 이질감이 드는 현상이 나타난다.
여기서 SSR을 사용함으로써 초기 렌더링 전에 필요한 로그인 데이터를 서버에서 받아와서
브라우저로 짠! 하게 나타내면 그러한 이질감이 있는 현상은 없게된다.

여기서 Next.js는 리액트에서 SSR을 보다 쉽게 사용할 수 있도록 해주는 도구다.
물론 SSR을 떠나 라우팅 기능도 있어서 너무 편하다!

하지만 이번 시간에서 서버사이드렌더링은 다루지 않는다.
추후 백엔드까지 내용을 정리하면서 올릴 예정이다.

Next.js install

npm i next (typescript 지원)

Next에서는 브라우저에서 노출되는 페이지는 모두 pages라는 폴더에 있어야한다.

pages
-- index.tsx
-- signup.tsx
-- profile.tsx
-- _app.tsx

위 처럼 pages 폴더를 생성해서 노출 될 페이지 파일을 넣어주면 된다.
또한 index.tsx를 만들어야 한다. 이 부분은 첫 렌더링 시 노출되는 페이지라고 생각하면 된다.
그리고 추가적으로 _app.tsx 생성해주자.

_app - pages/_app.tsx

이름은 꼭 저렇게 지어야할까? 그렇게 해야한다. 이건 Next.js 공식에서 정한 내용이다.
이 페이지는 기본 HTML 페이지를 전역으로 재정의하는 부분이라고 생각하면 좋다.
그래서 이 페이지에서 reset할 css파일 불러오면 전역으로 사용이 가능하다.

import React from 'react';
import Head from 'next/head'; // html head태그
import '../styles.css'; // reset.css
import wrapper from '../store/configureStore'; // redux store

// 컴포넌트 이름은 App이 아니어도 된다.
const App = ({ Component, pageProps }: any) => {
  return (
    <>
      <Head>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
        />
        <meta httpEquiv="X-UA-Compatible" content="ie=edge" />
        <title>React LogIn</title>
      </Head>
      <Component {...pageProps} />
    </>
  );
};

// HOC (Higher Order Component)
export default wrapper.withRedux(App);

여기서 props로 받은 Component pageProps는 뭘까?

사실 이 부분은 정확히 몰라서 공식문서를 찾아보았다. 당연히 파파고로 번역해서 알아보았다.

Component는 쉽게 pages 폴더안에 있는 파일을 가져오는 역할로 생각이 든다.
pageProps는 데이터를 가져오는 방법이라고 번역이 되지만,

정확한 내용은 추후 다시 정리를 하도록 하겠다.

index - pages/index.tsx

index.tsx 또한 Next 공식팀에서 정한 파일 명이다. 따르도록 하자.
위 파일은 http://localhost:3000/ 의 '/'라고 생각하면 된다. 즉, 메인 페이지로 제작을 하자.

여기서는 자유롭게 메인 페이지에 맞게 작업을 진행하면 된다.

import React from 'react';
import { useSelector } from 'react-redux';

import { Container } from '../styles/style';
import { loadUser } from 'actions/user';
import { RootState } from '../slices';

import LoginForm from '../component/loginForm';
import Profile from './profile';

const Home = () => {
  // 로그인 상태 체크
  const isLoggedIn = useSelector((state: RootState) => state.user.isLoggedIn);
	
  // 로그인 true면 프로필페이지, false면 로그인페이지
  return <Container>{isLoggedIn ? <Profile /> : <LoginForm />}</Container>;
};

export default Home;

다른 페이지는 생략하고, 추후 깃헙에 추가할 예정이다.

자유롭다. 이것은 타입스크립트가 아닌 그냥 자바스크립트...

Redux Toolkit

리덕스툴킷을 리덕스 공식팀에서 그 동안 리덕스에서 많이 사용된 기능을 비교적 쉽게 사용할 수 있도록
만든 상태관리 도구다. 리덕스와 리덕스 사가로 할 수도 있겠지만 리덕스 툴킷으로 정했다.

이유는 크게 없었다. 리덕스 툴킷을 좀 더 익히고자 진행을 했다.

redux toolkit install

npm i @reduxjs/toolkit (typescript 지원)
npm i next-redux-wrapper (Next 라이프사이클에 리덕스를 결합)

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import logger from 'redux-logger'; // 리덕스 로깅 라이브러리

import rootReducer from '../slices';

// 개발모드 체크
const isDev = process.env.NODE_ENV === 'development';

const createStore = () => {
  const middleware = getDefaultMiddleware();
  if (isDev) {
    middleware.push(logger); // 개발모드라면 미들웨어에 logger 추가
  }
  const store = configureStore({
    reducer: rootReducer,
    middleware,
    devTools: isDev, // 개발모드라면 리덕스 데브툴즈 사용
  });
  return store;
};

const wrapper = createWrapper(createStore, {
  debug: isDev,
});

export default wrapper;

타입스크립트 지원인지 아닌지 어떻게 알죠?**

많은 분들께서도 이미 아시겠지만, 간단해서 한번 소개하려구요 ㅎㅎ
저 같은 경우에는 npm 사이트에서 확인을 합니다.

npm 사이트로 이동해서 검색창에 타입스크립트 작업에서 사용하고자 하는 라이브러리의 명을 적어서
찾은 후 클릭해서 해당 라이브러리 페이지로 이동을 해주세요.

빨간색 박스를 보면 해당 라이브러리 옆에 파란색박스 TS라고 적혀있으면 타입스크립트를 지원해줍니다.
그게 아니라면 아래 처럼 흰색박스에 DT라고 적혀있습니다.

npm i @types/라이브러리명... 하여 타입스크립트용 추가 설치 해주시면 됩니다.

createSlice - slices/user.ts

기존 리덕스와 리덕스 사가는 많은 코드량과 따로 폴더를 만들어주기 때문에 이리저리 왔다갔다
작업하기 때문에 번거로운 점이 있었지만, createSlice로 편하게 할 수 있습니다.

물론 작업이 많아지면 당연히 코드량도 많아지겠지만..

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { signup, logIn, logOut, loadUser } from '../actions/user';

export interface User {
  nickname?: string;
  email: string;
  password: string;
}

const initialState = {
  user: <User>{
    nickname: '',
    email: '',
    password: '',
  },
  isLoggedIn: false,
  logInError: '',
  signupError: '',
  signupDone: false,
  isLoading: false,
};

const userSlice = createSlice({
  name: 'user', // name은 Slice와 맞고, 기억되기 쉬운걸로 지어주자.
  initialState, // 위 초기 상태값 가져오기
  reducers: {},
  extraReducers: builder => { // builder .addCase는 타입 추론에 용이하다.
    builder
      // 회원가입
      .addCase(signup.pending, (state, action) => {
        console.log('pending');
      })
      .addCase(signup.fulfilled, (state, action) => {
        console.log(action.payload);
        state.signupDone = true;
      })
      .addCase(signup.rejected, (state, action: PayloadAction<any>) => {
        state.signupError = action.payload;
      })
      // 로그인
      .addCase(logIn.pending, (state, action) => {
        state.isLoading = true;
      })
      .addCase(logIn.fulfilled, (state, action) => {
        state.isLoading = false;
        state.isLoggedIn = true;
        state.user.email = action.payload.email;
        state.user.nickname = action.payload.nick;
      })
      .addCase(logIn.rejected, (state, action: PayloadAction<any>) => {
        state.isLoading = false;
        state.logInError = action.payload;
      })
      // 로그아웃
      .addCase(logOut.pending, (state, action) => {})
      .addCase(logOut.fulfilled, (state, action) => {
        state.isLoggedIn = false;
      })
      .addCase(logOut.rejected, (state, action) => {})
      // 로그인 상태 불러오기
      .addCase(loadUser.pending, (state, action) => {
        state.isLoading = true;
      })
      .addCase(loadUser.fulfilled, (state, action) => {
        state.isLoading = false;
        state.isLoggedIn = true;
        state.user.email = action.payload.email;
        state.user.nickname = action.payload.nick;
      })
      .addCase(loadUser.rejected, (state, action) => {
        state.isLoading = false;
      });
  },
});

export default userSlice;

extraReducers만 썼다??

extraReducers는 보이다시피 요청/성공/실패 비동기 작업을 할 경우에 쓰인다.
그 외에 함수는 모두 reducers에 작업을 하면 된다.

pending - 로딩과 비슷한 맥락으로 요청 중으로 생각하면 된다.
fulfilled - 요청에 대한 응답 성공
rejected - 요청에 대한 응답 실패

위 세 가지의 이름은 리덕스에서 정한 이름이기 때문에 똑같이 적어야 한다.

rootReducer - slices/index.ts

import { combineReducers } from 'redux';

import userSlice from './user';

// slice는 user만 존재한다. 그래도 만들어서 store로 보내줬다.
const rootReducer = combineReducers({
  user: userSlice.reducer,
});

// 아래 타입은 구글링을 통해서 얻은 코드다..
// 이렇게 작성해주면 컴포넌트에서 useSelector를 통해서 쉽게 가져올 수 있다.
export type RootState = ReturnType<typeof rootReducer>;

export default rootReducer;

createAsyncThunk - actions/user

리덕스에서 비동기 작업을 할 경우에 Thunk를 사용하여 진행을 했다.
아니면 redux-saga를 사용하거나, 리덕스 툴킷에서는 createAsycThunk를 지원한다.

import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

import { User } from '../slices/user';

// 중복된 주소를 줄이는 방법
axios.defaults.baseURL = 'http://localhost:3051';
axios.defaults.withCredentials = true; // front, back 간 쿠키 공유

// 회원가입
export const signup = createAsyncThunk(
  'user/signup',
  async (data: User, { rejectWithValue }) => {
    try {
      const response = await axios.post('/signup', data);
      return response.data;
    } catch (error) {
      console.log(error);
      return rejectWithValue(error.response.data);
    }
  }
);

// 로그인
export const logIn = createAsyncThunk(
  '/user/logIn',
  async (data: User, { rejectWithValue }) => {
    try {
      const response = await axios.post('/login', data);
      return response.data;
    } catch (error) {
      console.log(error);
      return rejectWithValue(error.response.data);
    }
  }
);

// 로그아웃
export const logOut = createAsyncThunk('/user/logOut', async () => {
  const response = await axios.post('/logout');
  return response.data;
});

// 로그인 상태 불러오기
export const loadUser = createAsyncThunk('/user/load', async () => {
  const response = await axios.get('/user');
  console.log(response.data);
  return response.data;
});

비동기 작업은 모두 위 파일에 작업을 했다. 리덕스 사가였다면, 자바스크립트 제너레이터 문법과
위 코드처럼 try catch문 엄청 코드 길이가 길었겠지만, 뭔가 딱봐도 깔끔하게 각 영역마다
잘 읽히는게 좋다. 그럼 살짝 뜯어서 봐보자.

// thunk 사용하기 위해서 불러오기
const { createAsyncThunk } from '@reduxjs/toolkit;

export const 함수명 = createAsyncThunk('변수명', async () => {
  const response = await axios.메소드(요청서버주소);
  return response.data;
}); 

함수명 - 함수의 이름을 마음대로 정한다.
변수명 - 여기서 변수명이라는건 다시 Slice를 생각해봐야한다. extraReducers로 비동기 요청/성공/실패에 대해서 작업을 했다. 그럼 예를 들어서 변수명에 user를 넣겠다.

요청 - user/pending
성공 - user/fulfilled
실패 - user/rejected

이런식으로 앞에는 변수명, 뒤에는 응답에 대한 결과가 로깅에 찍힌다. 이 로깅은 리덕스 데브툴즈나
redux store에서 사용한 logger 미들웨어에서도 위와 같이 찍혀서 로깅된다.

메소드는 get, post, put, delete 등 상황에 맞는 메소드를 사용해주고,
요청을 보낼 서버 주소를 넣으면된다. 어차피 요청 보낼 서버는 어떤 비동기든 똑같은 주소다.
이럴땐 axios.defaults에서 중복을 없애는 작업을 해주면 된다.

axios.defaults.baseURL = "http://localhost:3000"

위 코드를 상단에 추가해주면, 요청서버주소에 기본값을 위 URL이 들어가있다.
추후에 서버와 DB까지 구축을 하고, 프론트와 백 서버간에 쿠키를 공유할 때는 하나 더 추가할 수 있다.

axios.defaults.withCredentials = true;

이렇게 2개의 코드를 상단에 추가해주면 된다. 물론 withCredentials는 백에서도 추가해주자.

return값으로 response.data는 백에서 응답에 data를 보면 우리가 원하는 데이터가 있다.
그것만 받아서 slice에서 action.payload로 받아서 쓰면 된다.


뭔가 한번에 정리를 하려다보니 뒤죽박죽인 것 같다. 나눠서 올려야겠다.

이번시간에는 Next.js의 기본 내용과 Redux Toolkit에 대해서 정리를 했다고
생각을 하면 될 것 같다. Slice나 Thunk에 적힌 코드를 여기서 보면 이해하기 힘들겠지만,
나눠서 올리면서 이해할 수 있도록 하겠다.

또한 Next는 웹팩 설정이 기본으로 들어있다. 때문에 이 작업에서는 웹팩은 따로 설정은 안했다.
아마 추후 배포과정에서는 필요할 것 같지만, 일단 그부분은 제외하고 진행할 예정이다.

다음 시간에는 pages, component에 대해서 올리겠습니다.

틀린 부분이 있다면 피드백 주시면 수정하도록 하겠습니다. 😔

profile
한 걸음 한걸음 / 현재는 알고리즘 공부 중!

2개의 댓글

comment-user-thumbnail
2021년 10월 8일

내용이 너무 좋네요.. 딱 해보고싶은 스펙이였는데 !!
파일 확장자나 디렉토리 구조를 자세히 표시해주시면 더 좋을것같아요
감사합니다!!

답글 달기
comment-user-thumbnail
2022년 5월 12일

많은 도움이 되었습니다~ 감사합니다~

답글 달기