
React + TypeScript + TailwindCSS 로그인 구현Front-End2024. 5. 17. 22:23
Table of Contents
지난 포스팅에서 Spring Security + Jwt + Redis 를 이용한 로그인을 구현해봤습니다. 이번 포스팅에서는 지난 포스팅과 통신하는 프론트 부분을 구현해보겠습니다.
▼ 지난 포스팅이 궁금하신 분은 아래의 링크를 확인해주세요 😊
Spring Security + JWT + Redis
지난 포스팅에서는 JWT(JSON Web Tokens)를 세션(Session)에서 관리하고 토큰이 만료될 때 로그인 시 사용할 수 있는 기능을 구현했습니다. 그러나 이 방식은 사용자가 자주 다시 인증을 해야 하기 때문
orijava.tistory.com
🛠 사용 IDE : Visual Studio Code
1. 프로젝트 초기화
npx create-react-app social-login-test --template typescript
cd .\social-login-test\
2. TailwindCSS 설치 및 설정
2-1. TailwindCSS 설치
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
2-2. TailwindCSS 기본 설정
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
- `tailwind.config.js` 파일을 수정해줍니다
@tailwind base;
@tailwind components;
@tailwind utilities;
- `src/index.css` 파일에 TailwindCSS 의 기본 스타일을 설정해줍니다
3. 컴포넌트 생성
3-1. `src/context/AuthContext.tsx`
- React 의 Context API 를 사용하여 전역적으로 인증 상태를 관리하는 역할을 합니다
- 인증 상태 관리 : 사용자 정보와 인증 토큰을 포함한 인증 상태를 관리합니다
- 상태 공유 : 인증 상태를 애플리케이션의 다른 컴포넌트에서 사용할 수 있도록 공유합니다
- 상태 업데이트 : 인증 상태를 업데이트할 수 있는 메서드를 제공합니다
import React, { createContext, useState, ReactNode } from "react";
interface AuthData {
id: string | null;
username: string | null;
nickname: string | null;
createdAt: string | null;
role: string | null;
token: string | null;
isAdmin: boolean;
}
interface AuthContextProps {
authData: AuthData;
setAuthData: React.Dispatch<React.SetStateAction<AuthData>>;
}
export const AuthContext = createContext<AuthContextProps | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [authData, setAuthData] = useState<AuthData>(() => {
const storedAuthData = localStorage.getItem('authData');
let initialAuthData = storedAuthData ? JSON.parse(storedAuthData) : {
id: null,
username: null,
nickname: null,
createdAt: null,
role: null,
token: localStorage.getItem("token") || null,
isAdmin: false
};
// 관리자 여부 확인
initialAuthData = {
...initialAuthData,
isAdmin: initialAuthData.role === 'ADMIN',
};
return initialAuthData;
});
return (
<AuthContext.Provider value={{ authData, setAuthData }}>
{children}
</AuthContext.Provider>
);
};
3-2. `src/services/AuthService.tsx`
- 인증 관련 API 요청을 처리하는 역할을 합니다
- API 요청 처리 : 로그인, 로그아웃, 회원가입 등의 인증 관련 API 요청을 처리합니다
- 에러 처리 : API 요청 실패 시 에러를 처리합니다
- 데이터 반환 : API 요청 성공 시 서버에서 반환된 데이터를 반환합니다
import axios from "axios";
const AuthService = {
signup: async (formData: { username: string; password: string; nickname: string; email: string }) => {
try {
const response = await axios.post(
"/api/join",
formData,
{
headers: {
"Content-Type": "application/json",
},
}
);
return response;
} catch (error: any) {
throw new Error("회원가입 실패: " + error.message);
}
},
};
export default AuthService;
3-3. `src/components/Join.tsx`
- 회원 가입 컴포넌트입니다
import React, { useState, useContext } from "react";
import AuthService from "../services/AuthService";
import { useNavigate } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
const Join: React.FC = () => {
const [formData, setFormData] = useState({
username: "",
password: "",
nickname: "",
email: "",
});
const [error, setError] = useState("");
const navigate = useNavigate();
const { setAuthData } = useContext(AuthContext)!;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await AuthService.signup(formData);
setAuthData({
...response.data,
isAdmin: response.data.role === 'ADMIN',
});
localStorage.setItem("authData", JSON.stringify(response.data));
localStorage.setItem("token", response.data.token);
navigate("/");
} catch (error: any) {
setError(error.message);
}
};
return (
<div className="flex items-center justify-center h-screen">
<form onSubmit={handleSubmit} className="bg-white p-6 rounded shadow-md w-80">
<h2 className="text-2xl font-bold mb-4">회원가입</h2>
{error && <p className="text-red-500 mb-4">{error}</p>}
<div className="mb-4">
<label htmlFor="username" className="block text-gray-700">아이디</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-gray-700">비밀번호</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="nickname" className="block text-gray-700">닉네임</label>
<input
type="text"
name="nickname"
value={formData.nickname}
onChange={handleChange}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="block text-gray-700">이메일</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<button type="submit" className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-700 transition">가입하기</button>
</form>
</div>
);
};
export default Join;
3-4. `src/components/Login.tsx`
- 로그인 컴포넌트입니다
import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
import axios from "axios";
const Login: React.FC = () => {
const [formData, setFormData] = useState({
username: "",
password: "",
});
const [error, setError] = useState("");
const navigate = useNavigate();
const { setAuthData } = useContext(AuthContext)!;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await axios.post(
"/api/login",
formData,
{
headers: {
"Content-Type": "application/json",
},
}
);
setAuthData({
...response.data,
isAdmin: response.data.role === 'ADMIN',
});
localStorage.setItem("authData", JSON.stringify(response.data));
localStorage.setItem("token", response.data.token);
navigate("/");
} catch (error: any) {
setError(error.message);
}
};
return (
<div className="flex items-center justify-center h-screen">
<form onSubmit={handleSubmit} className="bg-white p-6 rounded shadow-md w-80">
<h2 className="text-2xl font-bold mb-4">로그인</h2>
{error && <p className="text-red-500 mb-4">{error}</p>}
<div className="mb-4">
<label htmlFor="username" className="block text-gray-700">아이디</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-gray-700">비밀번호</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<button type="submit" className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-700 transition">로그인</button>
</form>
</div>
);
};
export default Login;
4. `src/index.tsx` 수정
- 모든 컴포넌트에서 인증을 처리할 수 있도록 `AuthProvider` 로 감싸줍니다
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AuthProvider } from './context/AuthContext';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
reportWebVitals();
5. `src/App.tsx` 수정
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Join from './components/Join';
import Login from './components/Login';
const App: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/join" Component={Join} />
<Route path="/login" Component={Login} />
<Route path="/" element={
<div className="flex items-center justify-center h-screen">
<h1 className="text-3xl font-bold">홈페이지</h1>
</div>
} />
</Routes>
</Router>
);
};
export default App;
6. package.json 에 proxy 추가
{
"name": "social-login-test",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.97",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"axios": "^1.6.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
// 추가된 부분
"proxy": "http://localhost:8081",
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3"
}
}
- `package.json` 파일에 프록시 설정을 추가하여 프론트엔트가 백엔드로 요청을 보내도록 설정해줍니다
7. 프론트엔드 서버 시작
npm start
8. 테스트
8-1. http://localhost:3000/join

8-2. http://localhost:3000/login

8-3. redis-cli

- 로그인 후 cli 를 통해 Acess Token 과 Refresh Token 이 정상적으로 저장이 되는 것을 확인해 볼 수 있습니다
TypeScript 는 정적 타입 검사를 통해 코드의 타입을 정해주고 반환값이 더 명시적으로 보여서 버그를 줄이고 안정적인 코드를 작성하기 좋은 것 같습니다
▼ 본 포스팅의 코드는 아래의 주소를 통해 확인하실 수 있습니다 (●'◡'●)
GitHub - bearkuang/React-TypeScript-TailwindCSS-Login: React+TypeScript+TailwindCSS+Login 을 이용한 간단한 컴포넌트
React+TypeScript+TailwindCSS+Login 을 이용한 간단한 컴포넌트 구현 - bearkuang/React-TypeScript-TailwindCSS-Login
github.com
'Front-End' 카테고리의 다른 글
Axios 를 이용한 로그인 구현하기 (0) | 2024.04.24 |
---|---|
Html 만들기 (0) | 2023.06.28 |
@BaekSJ :: 개발자의 길
Back-End, Front-End, DevOps 기록 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!