이번 시간에는 회원가입과 로그인을 구현을 진행한 작업에 대해서 정리를 하려고 한다.
React
Next.js
typescript
로 프론트를 작업하였고,
express
MySQL
sequelize
passport
로 백 부분을 작업했다.
백 서버와 DB를 구축해서 실제 저장된 계정으로 로그인을 하는 과정입니다.
로그아웃 시 비밀번호 일치하지 않습니다. 수정해야겠네요..😂 수정 끝~
깃헙에서 코드 확인 가능합니다.
투두앱을 제외한 프론트와 백을 구축해서 어떤 개인 실습을 해볼까 하다가 어느 서비스에나 존재하는
기능인 회원가입, 로그인을 구현하기로 하였다.
주의 : 타입스크립트라고 했지만 거의 자바스크립트에 가까운 코드입니다...😭
생각보다 타입스크립트... 후.. 하다보니 그냥 자바스크립트로 작업이 되었다...
Next.js
로 라우팅과 서버사이드렌더링(SSR)을 사용하였고, SSR은 이미 흔하게 사용되는 부분이기 때문에 특별하게 설명은 하지 않겠습니다.
그럼 여기에 왜 썼을까?
로그인이라 하면 로그인 후 어느 페이지를 가도 항상 로그인이 되어 있어야한다.
이 부분은 쿠키와 세션으로 처리는 되지만, 중요한건 고객의 사용경험이다.
리액트 기본적으로 CSR로 할 경우 페이지 업로드 후 필요한 로그인 데이터를 받아와서
로그인을 유지 시킨다. 그 사이에 깜빡거림(?) 같은 사용경험에 이질감이 드는 현상이 나타난다.
여기서 SSR을 사용함으로써 초기 렌더링 전에 필요한 로그인 데이터를 서버에서 받아와서
브라우저로 짠! 하게 나타내면 그러한 이질감이 있는 현상은 없게된다.
여기서 Next.js
는 리액트에서 SSR을 보다 쉽게 사용할 수 있도록 해주는 도구다.
물론 SSR을 떠나 라우팅 기능도 있어서 너무 편하다!
하지만 이번 시간에서 서버사이드렌더링은 다루지 않는다.
추후 백엔드까지 내용을 정리하면서 올릴 예정이다.
npm i next (typescript 지원)
Next에서는 브라우저에서 노출되는 페이지는 모두 pages
라는 폴더에 있어야한다.
pages
-- index.tsx
-- signup.tsx
-- profile.tsx
-- _app.tsx
위 처럼 pages
폴더를 생성해서 노출 될 페이지 파일을 넣어주면 된다.
또한 index.tsx
를 만들어야 한다. 이 부분은 첫 렌더링 시 노출되는 페이지라고 생각하면 된다.
그리고 추가적으로 _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.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;
다른 페이지는 생략하고, 추후 깃헙에 추가할 예정이다.
자유롭다. 이것은 타입스크립트가 아닌 그냥 자바스크립트...
리덕스툴킷을 리덕스 공식팀에서 그 동안 리덕스에서 많이 사용된 기능을 비교적 쉽게 사용할 수 있도록
만든 상태관리 도구다. 리덕스와 리덕스 사가로 할 수도 있겠지만 리덕스 툴킷으로 정했다.
이유는 크게 없었다. 리덕스 툴킷을 좀 더 익히고자 진행을 했다.
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
로 편하게 할 수 있습니다.
물론 작업이 많아지면 당연히 코드량도 많아지겠지만..
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
- 요청에 대한 응답 실패
위 세 가지의 이름은 리덕스에서 정한 이름이기 때문에 똑같이 적어야 한다.
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;
리덕스에서 비동기 작업을 할 경우에 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에 대해서 올리겠습니다.
틀린 부분이 있다면 피드백 주시면 수정하도록 하겠습니다. 😔
내용이 너무 좋네요.. 딱 해보고싶은 스펙이였는데 !!
파일 확장자나 디렉토리 구조를 자세히 표시해주시면 더 좋을것같아요
감사합니다!!