Vite + React : 간단한 로그인/로그아웃 및 Todo List 기능 구현
아래 포스팅은 Vite + React를 통해 간단한 로그인/로그아웃 및 Todo List 기능을 구현해 보는 예시입니다. 특히 로컬 스토리지(LocalStorage)를 활용하여, 사용자별 Todo 데이터가 로그인·로그아웃 후에도 유지되도록 구성했습니다. 개발자가 쉽게 따라 할 수 있도록 설치부터 하나씩 살펴보겠습니다.
Vite는 Vue.js의 개발자인 Evan You가 만든 차세대 프런트엔드 도구입니다. 빠르고 간결한 개발 경험을 제공하고 Webpack과 같은 기존 빌드 도구의 단점을 해결하는 것을 목표로 합니다.
1. 왜 Vite를 사용해야 하나요?
초고속 개발 : Vite의 개발 서버는 네이티브 ES 모듈을 사용하여 파일을 제공하므로 즉각적인 서버 시작과 번개처럼 빠른 핫 모듈 교체(HMR)가 가능합니다. 이는 브라우저에서 변경 사항을 확인하는 데 걸리는 시간을 줄여 개발 경험을 크게 개선합니다.
최적화된 빌드 : 프로덕션을 위해 Vite는 Rollup을 번들러로 사용하여 고도로 최적화되고 효율적인 빌드를 생성합니다.
유연성 : Vite는 React, Vue, Svelte, Preact를 포함한 여러 프런트엔드 프레임워크를 기본적으로 지원합니다. 플러그인 시스템을 통해 광범위한 사용자 정의 및 확장이 가능합니다.
2. Vite의 장점:
빠른 빌드 시간 : 즉각적인 서버 시작과 HMR로 개발 속도가 빨라집니다.
유연성 : 다양한 프레임워크를 지원하며 쉽게 확장할 수 있습니다.
최신 : 기본 ES 모듈과 최신 브라우저 API를 사용합니다.
3. Vite의 단점:
소규모 커뮤니티 : 비교적 새로운 지역으로 성장 중이지만 커뮤니티 규모는 작습니다.
제한된 SSR 지원 : 내장된 서버 측 렌더링 기능이 부족합니다.
브라우저 호환성 : 최신 브라우저 표준에 따라 제한됨.
4. Vite의 사용 사례:
단일 페이지 애플리케이션(SPA) : 빠른 개발과 최적화된 빌드에 이상적입니다.
라이브러리 및 구성 요소 개발 : Vite의 속도와 HMR은 재사용 가능한 구성 요소와 라이브러리를 개발하는 데 적합합니다.
프로토타입 제작 : 구성에 따른 간접비 없이 빠르게 새로운 프로젝트와 프로토타입을 만들어 보세요.
1. Vite + React 프로젝트 생성
1.1 Node.js / npm 설치 확인
- Node.js 공식 사이트에서 LTS(Long Term Support) 버전을 다운로드 및 설치합니다.
- 설치 후, 터미널(Windows 기준 CMD, PowerShell 등)에서 아래 명령어로 버전을 확인합니다.
node -v npm -v
- 버전이 정상적으로 뜨면 준비 완료!
1.2 Vite 프로젝트 생성
- 원하는 디렉터리(예:
D:\Source
)로 이동한 뒤, 다음 명령어를 실행합니다.npm create vite@latest my-vite-app -- --template react
my-vite-app
폴더가 생성되고, React 템플릿이 적용된 Vite 프로젝트가 준비됩니다.
- 폴더로 이동 후, 의존성을 설치합니다.
cd my-vite-app npm install
1.3 폴더 구조(기본)
프로젝트 생성 완료 시, 대략 아래와 유사한 폴더 구조가 생깁니다.
my-vite-app/
├─ node_modules/
├─ public/
├─ src/
│ ├─ App.jsx
│ └─ main.jsx
├─ index.html
├─ package.json
├─ vite.config.js
└─ ...
이제 여기서 로그인/로그아웃 및 Todo List 예제를 위해 폴더와 파일을 조금 더 세분화해보겠습니다.
2. 기본 기능 설명
여기서는 크게 로그인/로그아웃(Auth) 과 Todo 관리(Todo) 를 분리하여 관리합니다.
- AuthContext: 로그인/로그아웃 상태, 현재 사용자 정보를 전역으로 관리
- TodoContext: 사용자별 Todo 리스트를 전역으로 관리하고, 로컬 스토리지에 저장
- pages: 페이지 단위 컴포넌트(홈, 로그인, 투두페이지 등)
- components: 재사용 가능한 컴포넌트(네비게이션 바, Todo 입력/리스트 등)
- hooks: Context를 편리하게 사용할 수 있는 커스텀 훅
3. 폴더 구조 세분화
예시 폴더 구조
my-vite-app/
├── public/
├── src/
│ ├── pages/
│ │ ├── HomePage.jsx
│ │ ├── LoginPage.jsx
│ │ └── TodoPage.jsx
│ ├── components/
│ │ ├── Navbar.jsx
│ │ ├── TodoInput.jsx
│ │ └── TodoList.jsx
│ ├── contexts/
│ │ ├── AuthContext.jsx
│ │ └── TodoContext.jsx
│ ├── hooks/
│ │ ├── useAuth.js
│ │ └── useTodos.js
│ ├── App.jsx
│ └── main.jsx
├── index.html
├── package.json
├── vite.config.js
└── ...
4. AuthContext: 로그인/로그아웃 구현
로컬 스토리지를 사용하여 “어떤 사용자가 로그인했는지” 정보를 저장하고, 페이지 새로고침 후에도 유지될 수 있게 만듭니다.
4.1 AuthContext.jsx
// src/contexts/AuthContext.jsx
import { createContext, useState, useEffect } from 'react';
export const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
// 페이지 새로고침 시 localStorage에서 기존 로그인 정보 복원
useEffect(() => {
const storedUser = localStorage.getItem('authUser');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);
// 로그인 함수
const login = (userData) => {
setUser(userData);
localStorage.setItem('authUser', JSON.stringify(userData));
};
// 로그아웃 함수
const logout = () => {
setUser(null);
localStorage.removeItem('authUser');
};
// 로그인 여부
const isLoggedIn = !!user;
const authValue = {
user,
isLoggedIn,
login,
logout,
};
return (
<AuthContext.Provider value={authValue}>
{children}
</AuthContext.Provider>
);
}
- user: 현재 로그인된 사용자 정보(예:
{ id, name, token }
). - login, logout: 로그인/로그아웃 시 로컬 스토리지에 정보를 저장·삭제.
- isLoggedIn:
user
객체가 있으면true
, 없으면false
.
4.2 useAuth.js (커스텀 훅)
// src/hooks/useAuth.js
import { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';
export function useAuth() {
return useContext(AuthContext);
}
- 컴포넌트에서
useAuth()
를 호출하면 편하게user
,isLoggedIn
,login
,logout
등을 사용할 수 있습니다.
5. TodoContext: 사용자별 Todo 관리
5.1 TodoContext.jsx
// src/contexts/TodoContext.jsx
import { createContext, useState, useEffect, useCallback } from 'react';
import { useAuth } from '../hooks/useAuth';
export const TodoContext = createContext();
export function TodoProvider({ children }) {
const { user, isLoggedIn } = useAuth();
const [todos, setTodos] = useState([]);
// 사용자별로 저장할 todoData = { [userId]: [...todoList], ... }
const loadTodosForUser = useCallback((userId) => {
const storedData = localStorage.getItem('todoData');
if (!storedData) return [];
try {
const parsed = JSON.parse(storedData);
return parsed[userId] || [];
} catch (error) {
console.error('Failed to parse todoData:', error);
return [];
}
}, []);
const saveTodosForUser = useCallback((userId, updatedTodos) => {
const storedData = localStorage.getItem('todoData');
let parsed = {};
if (storedData) {
try {
parsed = JSON.parse(storedData);
} catch (error) {
console.error('Failed to parse todoData:', error);
}
}
parsed[userId] = updatedTodos;
localStorage.setItem('todoData', JSON.stringify(parsed));
}, []);
useEffect(() => {
if (isLoggedIn && user?.id) {
setTodos(loadTodosForUser(user.id));
} else {
setTodos([]);
}
}, [isLoggedIn, user?.id, loadTodosForUser]);
// Todo 추가
const addTodo = (text) => {
if (!isLoggedIn) return;
const newTodo = { id: Date.now(), text, done: false };
const updated = [...todos, newTodo];
setTodos(updated);
saveTodosForUser(user.id, updated);
};
// Todo 완료여부 토글
const toggleTodo = (id) => {
if (!isLoggedIn) return;
const updated = todos.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
);
setTodos(updated);
saveTodosForUser(user.id, updated);
};
// Todo 삭제
const removeTodo = (id) => {
if (!isLoggedIn) return;
const updated = todos.filter((t) => t.id !== id);
setTodos(updated);
saveTodosForUser(user.id, updated);
};
const value = {
todos,
addTodo,
toggleTodo,
removeTodo,
};
return (
<TodoContext.Provider value={value}>
{children}
</TodoContext.Provider>
);
}
핵심: user.id를 key로 해서 Todo 목록을 저장·로딩
- 이름이 같다면 동일한
user.id
로 간주하도록 구현(예:user.id = name
)- 실무에선 보통 이메일이나 DB의 userId를 사용
5.2 useTodos.js
// src/hooks/useTodos.js
import { useContext } from 'react';
import { TodoContext } from '../contexts/TodoContext';
export function useTodos() {
return useContext(TodoContext);
}
useTodos()
로todos
,addTodo
,toggleTodo
,removeTodo
를 편리하게 사용할 수 있습니다.
6. LoginPage & TodoPage 구성
6.1 LoginPage.jsx
// src/pages/LoginPage.jsx
import { useState } from 'react';
import { useAuth } from '../hooks/useAuth';
function LoginPage() {
const { login, logout, user, isLoggedIn } = useAuth();
const [name, setName] = useState('');
const handleLogin = () => {
if (!name.trim()) return;
// 같은 이름이면 같은 user.id가 되도록
const mockUser = {
id: name, // 실무에선 이메일/DB userId
name,
token: 'sample_token_' + Date.now(),
};
login(mockUser);
};
const handleLogout = () => {
logout();
};
return (
<div>
<h2>로그인 페이지</h2>
{isLoggedIn ? (
<>
<p>안녕하세요, {user.name}님!</p>
<button onClick={handleLogout}>로그아웃</button>
</>
) : (
<>
<input
type="text"
placeholder="이름을 입력하세요"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={handleLogin}>로그인</button>
</>
)}
</div>
);
}
export default LoginPage;
- “홍길동”이라는 이름으로 로그인하면
user.id = "홍길동"
이 됩니다. - 같은 이름으로 재로그인하면 이전에 작성한 Todo 목록을 불러옵니다.
6.2 TodoPage.jsx
// src/pages/TodoPage.jsx
import { useAuth } from '../hooks/useAuth';
import { useTodos } from '../hooks/useTodos';
import TodoInput from '../components/TodoInput';
import TodoList from '../components/TodoList';
function TodoPage() {
const { isLoggedIn } = useAuth();
const { todos } = useTodos();
if (!isLoggedIn) {
return (
<div>
<h2>접근 불가</h2>
<p>로그인 후 Todo를 작성할 수 있습니다.</p>
</div>
);
}
return (
<div>
<h2>Todo List</h2>
<TodoInput />
<TodoList />
<p>총 {todos.length} 개</p>
</div>
);
}
export default TodoPage;
- 비로그인 시 접근 불가 메시지를 표시합니다.
7. 컴포넌트 구성
7.1 Navbar.jsx
// src/components/Navbar.jsx
import { Link } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
function Navbar() {
const { isLoggedIn, user } = useAuth();
return (
<nav style={{ marginBottom: '1rem', borderBottom: '1px solid #ccc' }}>
<Link to="/" style={{ marginRight: '1rem' }}>
Home
</Link>
<Link to="/todos" style={{ marginRight: '1rem' }}>
Todo List
</Link>
<Link to="/login" style={{ marginRight: '1rem' }}>
{isLoggedIn ? '내 정보' : '로그인'}
</Link>
{isLoggedIn && <span> ( {user?.name} 님 )</span>}
</nav>
);
}
export default Navbar;
7.2 TodoInput.jsx
// src/components/TodoInput.jsx
import { useState } from 'react';
import { useTodos } from '../hooks/useTodos';
function TodoInput() {
const { addTodo } = useTodos();
const [value, setValue] = useState('');
const onSubmit = (e) => {
e.preventDefault();
if (!value.trim()) return;
addTodo(value);
setValue('');
};
return (
<form onSubmit={onSubmit} style={{ marginBottom: '1rem' }}>
<input
type="text"
placeholder="할 일을 입력하세요..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit">추가</button>
</form>
);
}
export default TodoInput;
7.3 TodoList.jsx
// src/components/TodoList.jsx
import { useTodos } from '../hooks/useTodos';
function TodoList() {
const { todos, toggleTodo, removeTodo } = useTodos();
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map((todo) => (
<li key={todo.id} style={{ margin: '0.5rem 0' }}>
<span
onClick={() => toggleTodo(todo.id)}
style={{
textDecoration: todo.done ? 'line-through' : 'none',
marginRight: '0.5rem',
cursor: 'pointer',
}}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
);
}
export default TodoList;
8. 라우팅 설정
8.1 HomePage.jsx
// src/pages/HomePage.jsx
function HomePage() {
return (
<div>
<h2>메인 화면</h2>
<p>이곳은 프로젝트 메인 페이지입니다. 상단 메뉴를 이용해 Todo, 로그인 페이지로 이동해 보세요.</p>
</div>
);
}
export default HomePage;
8.2 App.jsx
// src/App.jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import HomePage from './pages/HomePage';
import TodoPage from './pages/TodoPage';
import LoginPage from './pages/LoginPage';
function App() {
return (
<Router>
<Navbar />
<div style={{ padding: '0 1rem' }}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/todos" element={<TodoPage />} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</div>
</Router>
);
}
export default App;
React Router가 필요하므로 npm install react-router-dom을 미리 해주셔야 합니다.
9. main.jsx에서 Provider 등록
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from './contexts/AuthContext';
import { TodoProvider } from './contexts/TodoContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthProvider>
<TodoProvider>
<App />
</TodoProvider>
</AuthProvider>
</React.StrictMode>
);
- AuthProvider → TodoProvider 순으로 감싸서, 모든 페이지에서
useAuth()
,useTodos()
를 사용할 수 있게 합니다.
10. 실행 및 테스트
10.1 개발 서버 실행
npm run dev
- 브라우저에서 http://localhost:5173 접속
10.2 시나리오
- 로그인
- “홍길동” 입력 후 로그인 → 상단 Navbar에
( 홍길동 님 )
표시
- “홍길동” 입력 후 로그인 → 상단 Navbar에
- Todo 추가
- “React 학습하기”, “Vite 프로젝트 구성하기” 등 할 일을 추가
- Todo 항목을 클릭하면 완료/미완료가 토글
- 로그아웃 후 재로그인
- 로그아웃 → 다시 “홍길동” 로그인
- 이전에 작성한 Todo가 그대로 남아있어야 함(로컬 스토리지 기반)
- 다른 이름으로 로그인
- 예: “김철수”로 로그인하면 “홍길동”과 다른 Todo 목록을 관리
- 로컬 스토리지에서 ID 별로 분리되어 저장됨
11. 확장 아이디어
- 서버 연동
- 실제로는 DB와 연동하여 사용자 및 Todo 데이터를 주고받아야 합니다.
- JWT(JSON Web Token)나 세션을 통한 보안 고려가 필수.
- Protected Route
- TodoPage 접근 시, 로그인 안 되어 있으면 자동으로 /login 리다이렉트 등 “라우터 단위 보호” 기법을 적용해볼 수 있습니다.
- 다양한 기능 추가
- 회원가입, 비밀번호 인증, Social Login 등.
- Redux나 React Query 같은 전문 상태관리 라이브러리를 접목할 수도 있습니다.
- 이름 대신 이메일
- 로그인 시 “이메일”을 아이디로 사용하면, 좀 더 현실적인 시나리오에 가까워집니다.
12. 마무리
이처럼 Vite + React 환경에서 로그인/로그아웃과 Todo 리스트를 구현해 보았습니다.
- AuthContext를 통해 로그인 상태, 사용자 정보를 전역으로 관리
- TodoContext를 통해 사용자별로 Todo를 분리
- 로컬 스토리를 사용해 로그아웃 후에도 데이터 유지
- React Router, Context API, 로컬 스토리지를 한 번에 연습하기에 좋은 예제
실무에서는 서버와 연동된 형태로 확장하여, 보다 안전하고 완결성 높은 “회원 관리 + 할 일 관리” 시스템을 만들 수 있습니다.
즐거운 코딩에 도움이 되길 바랍니다!
<소스참고>
https://github.com/shimpark/my-vite-todo-project