๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
์Šคํ„ฐ๋””/ํ˜ผ๊ณต์Šคํ„ฐ๋””

[ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๋Š” ๋ฐ”์ด๋ธŒ ์ฝ”๋”ฉ with ํด๋กœ๋“œ ์ฝ”๋“œ] 4์ฃผ์ฐจ

by moon101 2026. 2. 1.

4์ฃผ์ฐจ๋Š” ํด๋กœ๋“œ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํˆฌ๋‘์•ฑ๊ณผ ๊ฐ„๋‹จํ•œ ๊ฒŒ์ž„ ์•ฑ์„ ๋งŒ๋“œ๋Š” ๊ฑธ ํ•ด๋ดค๋‹ค. ํด๋กœ๋“œ ์ฝ”๋“œ๋Š” ํ„ฐ๋ฏธ๋„์—์„œ ์ž‘์—…ํ•ด์•ผ๋˜์„œ ์•„์ง ์ ์‘์ด ์ž˜ ์•ˆ๋œ๋‹ค. ํˆฌ๋‘ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ํ˜„์žฌ ๋‚ด ๊ฐœ์ธ ํฌํŠธํด๋ฆฌ์˜ค๋กœ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ๋งŒ๋“ค๊ณ  ์žˆ๋Š”๊ฒŒ ์žˆ์–ด์„œ ๊ทธ๊ฑธ ๋” ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉํ•ด๋ดค๋‹ค. ์‚ฌ์‹ค ํ”„๋ก ํŠธ ๋ถ€๋ถ„์€ ํด๋กœ๋“œ ์›น์—์„œ ๋งŒ๋“ ๊ฑฐ๋ผ์„œ ๊ฐœ์„ ์‚ฌํ•ญ์ด ์—†์„ ์ค„ ์•Œ์•˜๋Š”๋ฐ ์ˆ˜์ •ํ•ด์•ผ ๋  ๋ถ€๋ถ„์ด ๋„ˆ๋ฌด ๋งŽ์ด ๋‚˜์™”๋‹ค..ใ… ใ…  ํ”„๋ก ํŠธ ์–ด๋ ค์›Œ...

 

๊ทธ๋ž˜์„œ ํด๋กœ๋“œ ์ฝ”๋“œ์— ๊ฐ€์„œ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋‹ฌ๋ผ๊ณ  ํ–ˆ๋‹ค๋‹ˆ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฆฌ์ŠคํŠธ๋ฅผ ์คฌ๋‹ค. 

๐Ÿ› ๏ธ React ํ”„๋กœ์ ํŠธ ๊ฐœ์„  ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๐Ÿ“ ํ”„๋กœ์ ํŠธ: Productivity App (Todo + Pomodoro)


๐Ÿ”ด ๋†’์€ ์šฐ์„ ์ˆœ์œ„ (๋ณด์•ˆ/๊ธฐ๋Šฅ ๋ฌธ์ œ)

1. Protected Route ์ถ”๊ฐ€

  • [ ] ProtectedRoute.jsx ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
  • [ ] ๋กœ๊ทธ์ธ ์•ˆ ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ /todo ์ ‘๊ทผ ์‹œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  • [ ] App.jsx์—์„œ TodoPage๋ฅผ ProtectedRoute๋กœ ๊ฐ์‹ธ๊ธฐ
// components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { selectIsAuthenticated } from '../store/authSlice';

export function ProtectedRoute({ children }) {
  const isAuthenticated = useSelector(selectIsAuthenticated);
  return isAuthenticated ? children : <Navigate to="/" replace />;
}

2. handleToggleTodo ์„œ๋ฒ„ ๋™๊ธฐํ™”

  • [ ] ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ ์‹œ ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ ์ถ”๊ฐ€
  • [ ] ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ(Optimistic Update) ์ ์šฉ
  • [ ] ์—๋Ÿฌ ์‹œ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
const handleToggleTodo = async (id) => {
  const todo = todos.find(t => t.id === id);
  const previousTodos = [...todos];
  
  // ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ
  setTodos(todos.map(t => 
    t.id === id ? { ...t, completed: !t.completed } : t
  ));
  
  try {
    await axiosInstance.patch(`/todos/${id}`, { 
      completed: !todo.completed 
    });
  } catch (error) {
    // ์—๋Ÿฌ ์‹œ ๋กค๋ฐฑ
    setTodos(previousTodos);
    alert('๋ณ€๊ฒฝ ์‹คํŒจ');
  }
};

3. 401 ์—๋Ÿฌ ์‹œ ์ž๋™ ๋กœ๊ทธ์•„์›ƒ

  • [ ] axios.js์—์„œ 401 ์—๋Ÿฌ ์‹œ logout ๋””์ŠคํŒจ์น˜
  • [ ] ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
// api/axios.js ์ˆ˜์ •
import { logout } from '../store/authSlice';

if (error.response?.status === 401) {
  store.dispatch(logout());
  window.location.href = '/';
}

๐ŸŸก ์ค‘๊ฐ„ ์šฐ์„ ์ˆœ์œ„ (์ฝ”๋“œ ํ’ˆ์งˆ)

4. TodoPage ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ

  • [ ] components/PomodoroTimer.jsx ๋ถ„๋ฆฌ
  • [ ] components/PomodoroModal.jsx ๋ถ„๋ฆฌ
  • [ ] components/TodoList.jsx ๋ถ„๋ฆฌ
  • [ ] components/TodoItem.jsx ๋ถ„๋ฆฌ
  • [ ] components/AddTodoModal.jsx ๋ถ„๋ฆฌ
  • [ ] ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋กœ์ง ๋ถ„๋ฆฌ: hooks/usePomodoro.js

๋ชฉํ‘œ: TodoPage.jsx๋ฅผ 528์ค„ → 100์ค„ ์ดํ•˜๋กœ

5. ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ ๊ด€๋ฆฌ

  • [ ] API ํ˜ธ์ถœ ์‹œ ๋กœ๋”ฉ ์ƒํƒœ ์ถ”๊ฐ€
  • [ ] ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
  • [ ] ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž ์นœํ™”์  ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
  • [ ] ๋นˆ ์ƒํƒœ(Empty State) UI ๊ฐœ์„ 
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const fetchTodos = async () => {
  setIsLoading(true);
  setError(null);
  try {
    const response = await axiosInstance.get('/todos');
    setTodos(response.data);
  } catch (err) {
    setError('ํ•  ์ผ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
  } finally {
    setIsLoading(false);
  }
};

6. useEffect ์˜์กด์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ

  • [ ] handlePomodoroComplete๋ฅผ useCallback์œผ๋กœ ๊ฐ์‹ธ๊ธฐ
  • [ ] useEffect ์˜์กด์„ฑ ๋ฐฐ์—ด ์ •๋ฆฌ
  • [ ] ESLint exhaustive-deps ๊ฒฝ๊ณ  ํ•ด๊ฒฐ
const handlePomodoroComplete = useCallback(async () => {
  // ๊ธฐ์กด ๋กœ์ง
}, [selectedTodo, pomodoroMode]);

useEffect(() => {
  // ...
}, [isRunning, pomodoroTime, pomodoroMode, handlePomodoroComplete]);

7. DOM ์ง์ ‘ ์ ‘๊ทผ ์ œ๊ฑฐ

  • [ ] document.getElementById ์ œ๊ฑฐ
  • [ ] useRef ๋˜๋Š” ์ƒํƒœ(state)๋กœ ๋ณ€๊ฒฝ
// Before
const select = document.getElementById('todo-select');
handleStartPomodoroFromCenter(select.value);

// After
const [selectedTodoId, setSelectedTodoId] = useState('');

<select 
  value={selectedTodoId} 
  onChange={(e) => setSelectedTodoId(e.target.value)}
>

๐ŸŸข ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ (์ผ๊ด€์„ฑ/์ •๋ฆฌ)

8. ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ต์ผ

  • [ ] ์ด๋ชจ์ง€ vs lucide-react ์ค‘ ํ•˜๋‚˜ ์„ ํƒ
  • [ ] LoginPage ์•„์ด์ฝ˜ ์ˆ˜์ • (ํ˜„์žฌ: ์ด๋ชจ์ง€)
  • [ ] SignupPage์™€ ํ†ต์ผ

9. console.log ์ œ๊ฑฐ

  • [ ] TodoPage.jsx 349์ค„: console.log('Todo:', todo) ์ œ๊ฑฐ
  • [ ] TodoPage.jsx 215์ค„: console.log('๐Ÿš€ Calling backend:...') ์ œ๊ฑฐ
  • [ ] TodoPage.jsx 258์ค„: console.log('โฐ Timer complete!...') ์ œ๊ฑฐ
  • [ ] ๋ฐฐํฌ ์ „ ๋ชจ๋“  console.log ์ •๋ฆฌ

10. Deprecated API ๊ต์ฒด

  • [ ] onKeyPress  onKeyDown์œผ๋กœ ๋ณ€๊ฒฝ
    • LoginPage.jsx (91์ค„, 107์ค„)
    • SignupPage.jsx (89์ค„, 105์ค„, 120์ค„)
// Before
onKeyPress={handleKeyPress}

// After  
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}

11. Header ์ปดํฌ๋„ŒํŠธ ์™„์„ฑ

  • [ ] ๋น„์–ด์žˆ๋Š” Header.jsx ๊ตฌํ˜„ ๋˜๋Š” ์‚ญ์ œ
  • [ ] ํ•„์š”์‹œ ๊ณตํ†ต ๋„ค๋น„๊ฒŒ์ด์…˜ ์ถ”๊ฐ€

12. alert() → ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ

  • [ ] react-toastify ๋˜๋Š” ์ปค์Šคํ…€ ํ† ์ŠคํŠธ ์ ์šฉ
  • [ ] ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

๐Ÿ”ง ์ถ”๊ฐ€ ๊ถŒ์žฅ์‚ฌํ•ญ

ํด๋” ๊ตฌ์กฐ ๊ฐœ์„ 

src/
โ”œโ”€โ”€ api/
โ”‚   โ””โ”€โ”€ axios.js
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ common/
โ”‚   โ”‚   โ”œโ”€โ”€ Button.jsx
โ”‚   โ”‚   โ”œโ”€โ”€ Input.jsx
โ”‚   โ”‚   โ”œโ”€โ”€ Modal.jsx
โ”‚   โ”‚   โ””โ”€โ”€ LoadingSpinner.jsx
โ”‚   โ”œโ”€โ”€ todo/
โ”‚   โ”‚   โ”œโ”€โ”€ TodoList.jsx
โ”‚   โ”‚   โ”œโ”€โ”€ TodoItem.jsx
โ”‚   โ”‚   โ””โ”€โ”€ AddTodoModal.jsx
โ”‚   โ””โ”€โ”€ pomodoro/
โ”‚       โ”œโ”€โ”€ PomodoroTimer.jsx
โ”‚       โ””โ”€โ”€ PomodoroModal.jsx
โ”œโ”€โ”€ hooks/
โ”‚   โ”œโ”€โ”€ usePomodoro.js
โ”‚   โ””โ”€โ”€ useTodos.js
โ”œโ”€โ”€ pages/
โ”‚   โ”œโ”€โ”€ LoginPage.jsx
โ”‚   โ”œโ”€โ”€ SignupPage.jsx
โ”‚   โ””โ”€โ”€ TodoPage.jsx
โ”œโ”€โ”€ store/
โ”‚   โ”œโ”€โ”€ store.js
โ”‚   โ””โ”€โ”€ authSlice.js
โ””โ”€โ”€ utils/
    โ””โ”€โ”€ formatTime.js

TypeScript ๋„์ž… ๊ณ ๋ ค

  • [ ] ํƒ€์ž… ์•ˆ์ •์„ฑ ํ™•๋ณด
  • [ ] ์ž๋™์™„์„ฑ ๋ฐ ๋ฆฌํŒฉํ† ๋ง ํŽธ์˜์„ฑ

ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€

  • [ ] Jest + React Testing Library ์„ค์ •
  • [ ] ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
  • [ ] API ํ˜ธ์ถœ ๋ชจํ‚น ํ…Œ์ŠคํŠธ

๐Ÿ“‹ ์ง„ํ–‰ ์ƒํ™ฉ

ํ•ญ๋ชฉ์ƒํƒœ์™„๋ฃŒ์ผ

Protected Route โฌœ -
์„œ๋ฒ„ ๋™๊ธฐํ™” โฌœ -
์ž๋™ ๋กœ๊ทธ์•„์›ƒ โฌœ -
์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ โฌœ -
๋กœ๋”ฉ ์ƒํƒœ โฌœ -
useEffect ์ˆ˜์ • โฌœ -
DOM ์ ‘๊ทผ ์ œ๊ฑฐ โฌœ -
์•„์ด์ฝ˜ ํ†ต์ผ โฌœ -
console.log ์ œ๊ฑฐ โฌœ -
onKeyPress ๊ต์ฒด โฌœ -
Header ์™„์„ฑ โฌœ -
ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ โฌœ -

 

 

๋””์ž์ธ ์ •๋„ ์ˆ˜์ •ํ•˜๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋งŽ์•„ ๋ณด์—ฌ์„œ vscode์—์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์—ด๊ณ  ํด๋กœ๋“œ ์ฝ”๋“œ๋ฅผ ํ„ฐ๋ฏธ๋„์— ํ‚ค๊ณ  1๋ฒˆ๋ถ€ํ„ฐ ์ˆ˜์ •ํ•ด๊ฐ”๋‹ค. ํด๋กœ๋“œ ์›น์—์„œ ํด๋กœ๋“œ ์ฝ”๋“œ์—์„œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ 1๋ฒˆ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ช…๋ น์–ด๋ฅผ ๋‹ฌ๋ผ๊ณ  ํ–ˆ๊ณ  

 

 

์•„๋ž˜ ๊ตฌ์ฒด์ ์ธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋„ฃ์–ด๋ดค๋‹ค. 

 

 

์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•  ๋•Œ๋งˆ๋‹ค ์–ด๋–ค๊ฑธ ์ˆ˜์ •ํ–ˆ๋Š”์ง€ ๋ณด์—ฌ์ฃผ๊ณ  

 

 

์ˆ˜์ •๋œ ๋ถ€๋ถ„์„ ๋ฐ˜์˜ํ•  ๊ฑด์ง€ ๋ฌผ์–ด๋ณธ๋‹ค. ๊ดœ์ฐฎ์€๊ฑฐ ๊ฐ™์•„์„œ yes ๋ˆ„๋ฆ„ ใ…Žใ…Ž 

 

์•Œ๋ ค์ค€ ์ˆ˜์ •์‚ฌํ•ญ์„ ๋‹ค ๋ฐ˜์˜ํ•˜๊ณ  ์‹ถ์—ˆ์ง€๋งŒ ์€๊ทผ ์‹œ๊ฐ„์ด ๊ฑธ๋ ค์„œ ์šฐ์„  ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋งŒ ๋ฐ˜์˜ํ–ˆ๋‹ค. ๋„๋ฉ”์ธ๋„ ์ƒ€๊ณ  Azure์—์„œ ๋ฐฐํฌํ•˜๋Š” ๊ฑฐ ์—ฐ์Šต ํ•ด๋ณด๊ณ  ์ง€๊ธˆ ๋งŒ๋“  ํˆฌ๋‘์•ฑ ๋ฐฐํฌ๊นŒ์ง€ ์ผ์ผ 1์ปค๋ฐ‹ ํ•˜๋ฉด์„œ ์–ผ๋ฅธ ํฌํด ๋งŒ๋“ค์–ด์•ผ์ง€.

 

ํ˜„์žฌ๊นŒ์ง€ ๋งŒ๋“ค์–ด์ง„ ํˆฌ๋‘์•ฑ ํ”„๋ก ํŠธ ํ™”๋ฉด

 

๋Œ“๊ธ€