Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 운영체제와 정보기술의 원리
- Git
- B tree 데이터삽입
- vite
- BreadcrumbsComputer-Networking_A-Top-Down-Approach
- 시스템프로그래밍
- 프로세스 주소 공간
- 백엔드
- 코딩애플
- 인터럽트
- 데이터베이스
- concurrency control
- 트랜잭션
- 김영한
- 시그널 핸들러
- 반효경
- SDK
- 쉬운 코드
- 쉬운코드
- 네트워크
- 운영체제
- CPU 스케줄링
- 코딩테스트 [ ALL IN ONE ]
- Extendable hashing
- 개발남노씨
- 갤럭시 S24
- recoverability
- 온디바이스AI
- SQL
- 커널 동기화
Archives
- Today
- Total
티끌모아 태산
회원가입 & 로그인 기능 구현(3) 본문
728x90
1. NavBar & Navbar Item
모든 페이지에 공통으로 들어가는 Navbar를 만들어 보겠다. frontEnd/src/layout/Navbar/index.jsx. 그리고 Navbar 안에 Sections 폴더를 만들어주어서 Navbar Item을 관리해준다.
// eslint-disable-next-line no-unused-vars
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import Navitem from './Sections/NavItem';
const Navbar = () => {
const [menu, setMenu] = useState(false); // menu가 처음에는 false로 초기화
// eslint-disable-next-line no-unused-vars
const handleMenu = () => {
// false면 true, true면 false, 토글
setMenu(!menu)
}
return (
// z-10은 relative를 위해서 주는 것
<section className='relative z-10 text-white bg-gray-900'>
<div className='w-full'>
{/* mx = margin left, right sm보다 크면 mx-10준다는 의미 */}
<div className='flex items-center justify-between mx-5 sm:mx-10 lg:mx-20'>
{/* logo */}
<div className='flex items-center text-2xl h-14'>
<Link to='/'>Logo</Link>
</div>
{/* menu */}
<div className='text-2xl sm:hidden'>
{/* 메뉴가 true이면 -, false이면 +, onClick하면 handleMenu 함수 호출 */}
<button onClick={handleMenu}>{menu ? '-' : '+'}</button>
</div>
{/* nav-items large screen 즉, small 보다 큰 사이즈 일 때 display:block처리, 기본이 display: none */}
<div className='hidden sm:block'>
<Navitem />
</div>
</div>
{/* nav-items mobile 즉, small보다 작은 사이즈 일 때 display:hidden */}
<div className='block sm:hidden'>
{/* menu가 true일때만 보여주는 것 */}
{menu && <Navitem mobile />}
</div>
</div>
</section>
)
}
export default Navbar
이제 NavItem 소스코드를 작성해보자
// eslint-disable-next-line no-unused-vars
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useNavigate } from 'react-router-dom';
import { logoutUser } from '../../../store/thunkFunctions';
const routes = [
{ to: '/login', name: '로그인', auth: false},
{ to: '/register', name: '회원가입', auth: false},
{ to: '', name: '로그아웃', auth: true },
]
// eslint-disable-next-line react/prop-types
const Navitem = ({ mobile }) => { mobile을 props로 내려받는다. 부모는 Navbar.jsx
const navigate = useNavigate();
//Redux 상태를 컴포넌트에서 선택적으로 가져오는 역할, 유저가 있을때만 가져온다는 뜻
const isAuth = useSelector(state => state.user?.isAuth)
//Redux 액션을 디스패치(dispatch)하는 함수를 가져오는 역할
const dispatch = useDispatch();
const handleLogout = async () => {
try {
dispatch(logoutUser()) // 1. 요청 보내기 2. 썽크함수 만들기 3. 썽크함수 등록하기
.then(() => {
navigate("/login")
})
} catch (error) {
console.error(error);
}
};
return (
// 모바일이 true 이면 flex-col
<ul className={`text-md justify-center w-full flex gap-4 ${mobile && "flex-col bg-gray-900 h-full"} items-center`}>
// map() 순회를 할 때 유니크한 키 값을 주어야 한다! 여기서는 name이 유니크하다.
{routes.map(({ to, name, auth, icon }) => { // 이렇게 중괄호를 해줘야 로직 작성가능, 소괄호()는 바로 렌더링하는 부분
if(isAuth !==auth) return null;
if(name === '로그아웃') {
return <li key={name} className='py-2 text-center border-b-4 cursor-pointer'>
<Link onClick={handleLogout}>{name}</Link>
</li>
} else {
// 고유 값으로 key={} 넣어줘야한다. 여기서는 name이 고유하므로 name을 넣는다.
return <li key={name} className='py-2 text-center border-b-4 cursor-pointer'>
<Link to={to}>{name}</Link>
</li>
}
})}
</ul>
)
}
export default Navitem
이제 로그아웃 기능을 위한 dispatch(logoutUser())에 대한 썽크 함수를 작성해주자.
import { createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../utils/axios";
export const registerUser = createAsyncThunk(
"user/registerUser",
async (body, thunkAPI) => {
try {
const response = await axiosInstance.post(
`/users/register`,
body
)
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
)
export const loginUser = createAsyncThunk(
"user/loginUser",
async (body, thunkAPI) => {
try {
const response = await axiosInstance.post(
`/users/login`,
body
)
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
)
export const authUser = createAsyncThunk(
// typePrefix
"user/authUser",
async (_, thunkAPI) => {
try {
const response = await axiosInstance.get(
`/users/auth`
);
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
)
export const logoutUser = createAsyncThunk(
"user/logoutUser",
async (_, thunkAPI) => {
try {
const response = await axiosInstance.post(
`/users/logout`
);
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
)
그러면 이 요청을 처리해주는 백엔드 라우트 부분을 작성해주어야 한다.
const express = require('express');
const User = require('../models/User');
const router = express.Router();
const jwt = require('jsonwebtoken');
const auth = require('../middleware/auth');
const async = require('async');
// 로그인이된 유저가 페이지를 이동할 때마다 여기로 요청이 온다. 로그인이 잘 된 유저면 데이터베이스에 있는 데이터를 클라이언트한테 전달하는 부분
router.get('/auth', auth, async (req, res, next) => {
return res.json({
id: req.user._id,
email: req.user.email,
name: req.user.name,
role: req.user.role,
image: req.user.image,
cart: req.user.cart,
history: req.user.history
})
})
router.post('/register', async (req, res, next) => {
try {
const user = new User(req.body); // 유저 객체 만들기, req.body안에 유저 정보가들어있다.
await user.save(); // 만들어진 유저객체에 save하면 자동으로 몽고디비에 유저정보가 저장된다.
return res.sendStatus(200); // 성공했다고 클라이언트에게 전달한다.
} catch (error) {
console.log('에러가 발생하였습니다.', error)
next(error)
}
})
router.post('/login', async (req, res, next) => {
// req.body password , email
try {
// 존재하는 유저인지 체크
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(400).send("Auth failed, email not found");
}
// 비밀번로가 올바른 것인지 체크
const isMatch = await user.comparePassword(req.body.password);
if (!isMatch) {
return res.status(400).send('Wrong password');
}
const payload = {
userId: user._id.toHexString(),
}
// token을 생성
const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' })
return res.json({ user, accessToken })
} catch (error) {
next(error)
}
})
// auth 미들웨어 통과하면 올바른 유저라는 뜻.
router.post('/logout', auth, async (req, res, next) => {
try {
return res.sendStatus(200);
} catch(error) {
next(error)
}
})
마지막으로 썽크함수를 userSlice.js 파일에 등록해주면 된다!
import {createSlice} from '@reduxjs/toolkit'
import { loginUser, registerUser, authUser, logoutUser, addToCart, getCartItems, removeCartItem, payProducts } from './thunkFunctions';
import { toast } from 'react-toastify';
// eslint-disable-next-line no-unused-vars
const initialState = {
userData: {
id: '',
email: '',
name: '',
role: '0', // 0은 일반유저, 1은 관리자
image: '',
},
isAuth: false,
isLoading: false, // 가져오는 중에는 True, 가져왔으면 false
error: ""
}
// eslint-disable-next-line no-undef
const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
// eslint-disable-next-line no-unused-vars
extraReducers: (builder) => {
builder
.addCase(registerUser.pending, (state) => {
state.isLoading = true;
})
.addCase(registerUser.fulfilled, (state) => {
state.isLoading = false;
toast.info('회원가입을 성공했습니다.');
})
.addCase(registerUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload);
})
.addCase(loginUser.pending, (state) => {
state.isLoading = true;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.isLoading = false;
state.userData = action.payload
state.isAuth = true;
localStorage.setItem('accessToken', action.payload.accessToken)
})
.addCase(loginUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload);
})
.addCase(authUser.pending, (state) => {
state.isLoading = true;
})
.addCase(authUser.fulfilled, (state, action) => {
state.isLoading = false;
state.userData = action.payload
state.isAuth = true;
})
.addCase(authUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
// 유저 데이터 초기화
state.userData = initialState.userData
state.isAuth = false;
// 만약 토큰이 만료된 사람이면
localStorage.removeItem('accessToken')
})
.addCase(logoutUser.pending, (state) => {
state.isLoading = true;
})
.addCase(logoutUser.fulfilled, (state) => {
state.isLoading = false;
state.userData = initialState.userData;
state.isAuth = false;
localStorage.removeItem('accessToken')
})
.addCase(logoutUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload)
})
2. Token 만료되었다면?
처음에 토큰 만료시간을 1시간으로 주었기 때문에 1시간이 지나면 만료가 된다. 따라서 리로드 해주는 함수를 작성해보자. axios.js에서 작성.
import axios from 'axios';
// eslint-disable-next-line no-unused-vars
const axiosInstance = axios.create({
baseURL: import.meta.env.PROD ?
"" : "http://localhost:4000",
})
axiosInstance.interceptors.request.use(function (config) {
// 요청을 보내기 전에 어떤것을 하고 싶을 때
config.headers.Authorization = 'Bearer ' + localStorage.getItem('accessToken');
return config;
}, function (error) { //에러 발생시 여기서 처리
return Promise.reject(error);
})
// 토큰이 1시간 후에 만료되는데, 이를 갱신하기 위한 코드
axiosInstance.interceptors.response.use(function (response) {
return response;
}, function (error) {
if(error.response.data === 'jwt expired') {
window.location.reload()
}
return Promise.reject(error);
})
export default axiosInstance;
따라하며 배우는 노드, 리액트 시리즈 - 쇼핑몰 사이트 만들기[전체 리뉴얼] - 인프런 | 강의
이 강의를 통해서 쇼핑몰 웹사이트를 처음부터 하나하나 만들어 보실 수 있습니다., 노드 + 리액트 개발, 한단계 레벨 업!쇼핑몰 웹사이트를 직접 만들어보세요. [사진] 안녕하세요. 이 강의가
www.inflearn.com
728x90