프로그래밍/ReactJS 🤞

Vite + React : 간단한 로그인/로그아웃 및 Todo List 기능 구현

재우니 2024. 12. 26. 02:31

아래 포스팅은 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>
);
  • AuthProviderTodoProvider 순으로 감싸서, 모든 페이지에서 useAuth(), useTodos()를 사용할 수 있게 합니다.

10. 실행 및 테스트

10.1 개발 서버 실행

npm run dev

10.2 시나리오

  1. 로그인
    • “홍길동” 입력 후 로그인 → 상단 Navbar에 ( 홍길동 님 ) 표시
  2. Todo 추가
    • “React 학습하기”, “Vite 프로젝트 구성하기” 등 할 일을 추가
    • Todo 항목을 클릭하면 완료/미완료가 토글
  3. 로그아웃 후 재로그인
    • 로그아웃 → 다시 “홍길동” 로그인
    • 이전에 작성한 Todo가 그대로 남아있어야 함(로컬 스토리지 기반)
  4. 다른 이름으로 로그인
    • 예: “김철수”로 로그인하면 “홍길동”과 다른 Todo 목록을 관리
    • 로컬 스토리지에서 ID 별로 분리되어 저장됨

11. 확장 아이디어

  1. 서버 연동
    • 실제로는 DB와 연동하여 사용자 및 Todo 데이터를 주고받아야 합니다.
    • JWT(JSON Web Token)나 세션을 통한 보안 고려가 필수.
  2. Protected Route
    • TodoPage 접근 시, 로그인 안 되어 있으면 자동으로 /login 리다이렉트 등 “라우터 단위 보호” 기법을 적용해볼 수 있습니다.
  3. 다양한 기능 추가
    • 회원가입, 비밀번호 인증, Social Login 등.
    • Redux나 React Query 같은 전문 상태관리 라이브러리를 접목할 수도 있습니다.
  4. 이름 대신 이메일
    • 로그인 시 “이메일”을 아이디로 사용하면, 좀 더 현실적인 시나리오에 가까워집니다.

12. 마무리

이처럼 Vite + React 환경에서 로그인/로그아웃Todo 리스트를 구현해 보았습니다.

 

  • AuthContext를 통해 로그인 상태, 사용자 정보를 전역으로 관리
  • TodoContext를 통해 사용자별로 Todo를 분리
  • 로컬 스토리를 사용해 로그아웃 후에도 데이터 유지
  • React Router, Context API, 로컬 스토리지를 한 번에 연습하기에 좋은 예제

 

실무에서는 서버와 연동된 형태로 확장하여, 보다 안전하고 완결성 높은 “회원 관리 + 할 일 관리” 시스템을 만들 수 있습니다.
즐거운 코딩에 도움이 되길 바랍니다!

 

 

<소스참고>

 

https://github.com/shimpark/my-vite-todo-project