TIL #41 심화주차 개인과제-2

DO YEON KIM·2024년 6월 12일
1

부트캠프

목록 보기
41/72

하루 하나씩 작성하는 TIL #41


필수 구현 사항 및 권장 진행 사항에 맞게 각 단계에 대해 설명해보도록 하겠다.


1. 지출 관리 시스템에 회원가입 / 로그인 기능 구현

  • 반드시, 강의에서 제공하는 jwt 인증서버를 사용하도록 합니다.
  • 인증이 되지 않는다면 서비스를 이용 할 수 없도록 해주세요.

회원가입

const register = async (credentials) => {
    try {
        await axios.post(`${API_URL}/register`, credentials);
    } catch (error) {
        throw new Error('회원가입 실패: ' + error.response.data.message);
    }
};

회원가입 요청을 JWT 인증 서버로 보내기.
xios.post 메소드를 사용하여 API_URL/register로 회원가입 요청을 보내준다.

로그인

const login = async (credentials) => {
    try {
        const response = await axios.post(`${API_URL}/login`, credentials);
        localStorage.setItem('token', response.data.accessToken);
        setUser(response.data);
        navigate('/');
    } catch (error) {
        throw new Error('로그인 실패: ' + error.response.data.message);
    }
};

로그인 요청을 JWT 인증 서버로 보내고, 서버로부터 받은 JWT 토큰을 localStorage에 저장

이 부분에서 axios.post 메소드를 사용하여 API_URL/login로 로그인 요청을 보내고, 응답으로 받은 accessToken을 localStorage에 저장한다.


2. json-server 를 이용해 지출 데이터에 대한 CRUD 를 구현

  • 지출 데이터에 누가 해당 지출을 생성 했는지가 포함시켜 봅시다.

C

const mutationAdd = useMutation({
    mutationFn: addExpense,
    onSuccess: () => {
        queryClient.invalidateQueries(['expenses']);
    }
});

useMutation를 사용하여 지출 데이터를 추가

R

const { data: expenses, error, isLoading } = useQuery({
    queryKey: ['expenses', month],
    queryFn: () => getExpenses(month)
});

useQuery를 사용하여 지출 데이터를 가져오기

U

const mutationUpdate = useMutation({
    mutationFn: updateExpense,
    onSuccess: () => {
        queryClient.invalidateQueries(['expenses']);
    }
});

useMutation를 사용하여 지출 데이터를 업데이트

D

const mutationDelete = useMutation({
    mutationFn: deleteExpense,
    onSuccess: () => {
        queryClient.invalidateQueries(['expenses']);
    }
});

useMutation를 사용하여 지출 데이터를 삭제

지출데이터에 사용자 정보를 포함

import { useAuth } from '../context/AuthContext';

// ...

export default function CreateExpense({ addExpense }) {
  const { user } = useAuth(); // 현재 로그인한 사용자 정보 가져오기

  // ...

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!user) {
      alert('로그인이 필요합니다.');
      return;
    }
    addExpense({
      date,
      item,
      amount,
      description,
      createdBy: user.nickname,
      userId: user.id
    });

    // ...
  };

  // ...
}

지출 데이터를 생성할 때, 현재 로그인 한 사용자의 정보를 포함시켜서 addExpense 함수로 전달

// 지출 항목 추가
export const addExpense = async (expense) => {
    const response = await axios.post(BASE_URL, expense);
    return response.data;
};

지출 데이터를 생성할 때 createdBy와 userId 정보를 포함하여 addExpense 함수로 전달


3. API 호출 시, fetch 대신 axios 를 필수적으로 사용

이건 위 코드에서도 보이기 때문에 생략하겠다.


4. 지출데이터 관련 API 통신 시 response 를 페이지에서 바로 사용하지 않도록 합니다. 꼭 Tanstack Query (ReactQuery) 를 이용해서 다뤄주세요.

  • 지출데이터의 상태 관리는 Props-drilling, Context API, Redux 사용대신 Tanstack Query 를 사용해야 합니다.

crud 파트에서 사용하는 모습을 확인 가능하기 때문에 생략하겠다.


5. JWT 인증 서버


6. 로그인, 회원가입 페이지 UI 작업 및 라우터 설정

  • react-router-dom 을 이용하여 로그인화면과 회원가입화면의 라우터 설정을 먼저 해봅시다.

  • 로그인창에서는 회원가입 버튼을 클릭하면 회원가입창으로, 회원가입창에서는 로그인 버튼을 누르면 로그인창으로 이동 되도록 구현해 보세요.

  • 아이디는 4~10글자로, 비밀번호는 4~15글자로, 닉네임은 1~10글자로 제한하세요.

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router>
        <AuthProvider>
          <div>
            <Navbar />
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/login" element={<Login />} />
              <Route path="/register" element={<Register />} />
              <Route path="/profile" element={<Profile />} />
            </Routes>
          </div>
        </AuthProvider>
      </Router>
    </QueryClientProvider>
  );
}

export default App;
import { Link } from 'react-router-dom';

const Login = () => {
  return (
    <div style={formContainerStyle}>
      <form onSubmit={handleSubmit} style={formStyle}>
        <h2 style={titleStyle}>로그인</h2>
        {/* ... */}
        <Link to="/register" style={registerLinkStyle}>회원가입</Link>
      </form>
    </div>
  );
};

export default Login;
import { Link } from 'react-router-dom';

const Register = () => {
  return (
    <div style={formContainerStyle}>
      <form onSubmit={handleSubmit} style={formStyle}>
        <h2 style={titleStyle}>회원가입</h2>
        {/* ... */}
        <Link to="/login" style={registerLinkStyle}>로그인</Link>
      </form>
    </div>
  );
};

export default Register;
const handleSubmit = async (e) => {
  e.preventDefault();

  if (credentials.id.length < 4 || credentials.id.length > 10) {
    setError('아이디는 4~10글자로 입력해주세요.');
    return;
  }
  if (credentials.password.length < 4 || credentials.password.length > 15) {
    setError('비밀번호는 4~15글자로 입력해주세요.');
    return;
  }

  try {
    await login(credentials);
  } catch (error) {
    setError(error.message);
  }
};

7. 로그인, 회원가입 화면 API 연결

로그인과 회원가입 API는 JWT 인증서버를 사용해야 합니다. API명세 를 참고해서 구현해 보세요. (API 요청은 fetch 대신 axios 를 사용합니다.)

import axios from 'axios';

const API_URL = 'https://moneyfulpublicpolicy.co.kr';

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  const login = async (credentials) => {
    try {
      const response = await axios.post(`${API_URL}/login`, credentials);
      localStorage.setItem('token', response.data.accessToken);
      setUser(response.data);
    } catch (error) {
      throw new Error('로그인 실패: ' + error.response.data.message);
    }
  };

  const register = async (credentials) => {
    try {
      await axios.post(`${API_URL}/register`, credentials);
    } catch (error) {
      throw new Error('회원가입 실패: ' + error.response.data.message);
    }
  };

  // Other functions...

  return (
    <AuthContext.Provider value={{ user, setUser, login, logout, register }}>
      {children}
    </AuthContext.Provider>
  );
};

login 함수와 register 함수가 axios를 사용하여 JWT 인증서버에 요청을 보냄.


회원가입을 먼저 구현해주세요. 성공했을 경우, 그리고 실패했을 경우 어떻게 응답값이 오는지 비교를 해봅시다.

그런 다음에 로그인을 구현합니다. 로그인에 성공하면 서버 응답으로 받은 accessToken 을 로컬스토리지에 저장합니다.

const handleSubmit = async (e) => {
  e.preventDefault();

  if (credentials.id.length < 4 || credentials.id.length > 10) {
    setError('아이디는 4~10글자로 입력해주세요.');
    return;
  }
  if (credentials.password.length < 4 || credentials.password.length > 15) {
    setError('비밀번호는 4~15글자로 입력해주세요.');
    return;
  }
  if (credentials.nickname.length < 1 || credentials.nickname.length > 10) {
    setError('닉네임은 1~10글자로 입력해주세요.');
    return;
  }

  try {
    await register(credentials);
    alert('회원가입 성공! 로그인 페이지로 이동합니다.');
    navigate('/login');
  } catch (error) {
    setError(error.message);
  }
};

성공 및 실패시 응답

const login = async (credentials) => {
  try {
    const response = await axios.post(`${API_URL}/login`, credentials);
    localStorage.setItem('token', response.data.accessToken);
    setUser(response.data);
  } catch (error) {
    throw new Error('로그인 실패: ' + error.response.data.message);
  }
};

로그인은 AuthContext.jsx의 login 함수에서 구현되어 있으며, 로그인 성공 시 accessToken을 로컬스토리지에 저장


새로고침시에도 로그인 상태가 유지되도록 로컬스토리지를 이용합니다.

useEffect(() => {
  const token = localStorage.getItem('token');
  if (token) {
    axios.get(`${API_URL}/user`, {
      headers: {
        Authorization: `Bearer ${token}`
      }
    }).then(response => {
      setUser(response.data);
    }).catch(() => {
      localStorage.removeItem('token');
      navigate('/login');
    });
  }
}, [navigate]);

새로고침 시 로그인 상태 유지


어플리케이션을 새로고침 했을 경우에 혹은 브라우저를 껐다가 다시 재접속을 했을 때, accessToken 을 바탕으로 회원정보 확인 API 호출하여 로그인이 유지가 될 수 있게 합니다. accessToken 이 만료가 되었거나 존재하지 않는다면, 로그인 페이지를 보여주면 되겠죠?

useEffect(() => {
  const token = localStorage.getItem('token');
  if (token) {
    axios.get(`${API_URL}/user`, {
      headers: {
        Authorization: `Bearer ${token}`
      }
    }).then(response => {
      setUser(response.data);
    }).catch(() => {
      localStorage.removeItem('token');
      navigate('/login');
    });
  }
}, [navigate]);

로그인 유지 및 accessToken 유효성 검사


로그인 또는 회원가입 실패 시 에러메시지를 사용자에게 보여줍니다.

const handleSubmit = async (e) => {
  e.preventDefault();

  if (credentials.id.length < 4 || credentials.id.length > 10) {
    setError('아이디는 4~10글자로 입력해주세요.');
    return;
  }
  if (credentials.password.length < 4 || credentials.password.length > 15) {
    setError('비밀번호는 4~15글자로 입력해주세요.');
    return;
  }

  try {
    await login(credentials);
  } catch (error) {
    setError(error.message);
  }
};

8. Header Navigation Bar 컴포넌트 작성

로그인에 성공했을 경우, 홈화면으로 이동시켜 줍니다.

const login = async (credentials) => {
  try {
    const response = await axios.post(`${API_URL}/login`, credentials);
    localStorage.setItem('token', response.data.accessToken);
    setUser(response.data);
    navigate('/'); // 홈화면으로 이동
  } catch (error) {
    throw new Error('로그인 실패: ' + error.response.data.message);
  }
};

로그인이 된 홈화면에는 Navigation Bar 나타나야 합니다. Layout.jsx 컴포넌트 작성하시고 그곳에서 Navigation Bar 를 구현해주세요. (로그인이 되지 않았을 때는 보여주지 않도록 해봅시다.)

import React from 'react';
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
import { useAuth } from '../context/AuthContext';

const Layout = () => {
  const { user } = useAuth();

  return (
    <div>
      {user && <Navbar />}
      <Outlet />
    </div>
  );
};

export default Layout;

최소한 프로필 변경을 할 수 있도록 “내 프로필” 메뉴와 로그아웃을 위한 “로그아웃” 메뉴 그리고 홈 화면으로 다시 이동할 수 있는 “HOME” 메뉴는 있어야 합니다.

return (
  <nav style={navStyle}>
    <div style={leftNavStyle}>
      <Link to="/">HOME</Link>
      <Link to="/profile">내 프로필</Link>
    </div>
    <div style={rightNavStyle}>
      <img src={user.avatar} alt="avatar" style={avatarStyle} />
      <span>{user.nickname}</span>
      <button onClick={logout} style={logoutButtonStyle}>로그아웃</button>
    </div>
  </nav>
);

로그아웃 과정에서 accessToken 까지 확실하게 지워주시는 것을 잊지 말아주세요. 로그아웃이 되면 로그인 페이지로 이동을 시켜주세요.

const logout = () => {
  localStorage.removeItem('token');
  setUser(null);
  navigate('/login'); // 로그아웃 시 로그인 페이지로 이동
};

내 프로필 메뉴에서는 접속한 사용자의 닉네임과 프로필사진을 변경 할 수 있는 UI 를 제공하도록 합시다. 방식은 자유입니다.

import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';

const Profile = () => {
  const { user, updateProfile } = useAuth();
  const [nickname, setNickname] = useState(user.nickname);
  const [avatar, setAvatar] = useState(null);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const formData = new FormData();
      formData.append('nickname', nickname);
      if (avatar) {
        formData.append('avatar', avatar);
      }
      await updateProfile(formData);
      alert('프로필 업데이트 완료!');
    } catch (error) {
      setError(error.message);
    }
  };

  return (
    <div style={formContainerStyle}>
      <form onSubmit={handleSubmit} style={formStyle}>
        <h2 style={titleStyle}>프로필 변경</h2>
        <div style={inputContainerStyle}>
          <label htmlFor="nickname">닉네임</label>
          <input
            type="text"
            name="nickname"
            value={nickname}
            onChange={(e) => setNickname(e.target.value)}
            style={inputStyle}
          />
        </div>
        <div style={inputContainerStyle}>
          <label htmlFor="avatar">프로필 이미지</label>
          <input
            type="file"
            name="avatar"
            onChange={(e) => setAvatar(e.target.files[0])}
            style={inputStyle}
          />
        </div>
        {error && <p style={errorStyle}>{error}</p>}
        <button type="submit" style={buttonStyle}>업데이트</button>
      </form>
    </div>
  );
};

프로필 변경 API 는 JWT 인증서버에서 제공하고 있습니다.

const updateProfile = async (profileData) => {
  const token = localStorage.getItem('token');
  try {
    const response = await axios.patch(`${API_URL}/profile`, profileData, {
      headers: {
        'Content-Type': 'multipart/form-data',
        Authorization: `Bearer ${token}`
      }
    });
    setUser(response.data);
  } catch (error) {
    throw new Error('프로필 업데이트 실패: ' + error.response.data.message);
  }
};

8. json-server 셋업


9. 지출 데이터에 지출을 등록한 유저가 누구인지 판단 할 수 있는 값 추가

지출이 생성 될 때 어느 사용자가 지출을 생성 했는지를 함께 저장해주도록 합시다. (유저의 id 이용)

import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';

export default function CreateExpense({ addExpense }) {
  const [date, setDate] = useState('');
  const [item, setItem] = useState('');
  const [amount, setAmount] = useState('');
  const [description, setDescription] = useState('');
  const { user } = useAuth();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (user) {
      addExpense({ date, item, amount, description, createdBy: user.nickname, userId: user.id });
    } else {
      alert('로그인이 필요합니다.');
    }
    setDate('');
    setItem('');
    setAmount('');
    setDescription('');
  };

  return (
    <form onSubmit={handleSubmit} style={formStyle}>
      <div style={inputContainerStyle}>
        <label style={labelStyle}>날짜</label>
        <input
          type="date"
          value={date}
          onChange={(e) => setDate(e.target.value)}
          style={inputStyle}
        />
      </div>
      <div style={inputContainerStyle}>
        <label style={labelStyle}>항목</label>
        <input
          type="text"
          placeholder="지출 항목"
          value={item}
          onChange={(e) => setItem(e.target.value)}
          style={inputStyle}
        />
      </div>
      <div style={inputContainerStyle}>
        <label style={labelStyle}>금액</label>
        <input
          type="number"
          placeholder="지출 금액"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          style={inputStyle}
        />
      </div>
      <div style={inputContainerStyle}>
        <label style={labelStyle}>내용</label>
        <input
          type="text"
          placeholder="지출 내용"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          style={inputStyle}
        />
      </div>
      <button type="submit" style={buttonStyle}>추가</button>
    </form>
  );
}

누구로부터 생성된 지출인지 알 수 있도록 구성

import { v4 as uuidv4 } from 'uuid';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useAuth } from '../context/AuthContext';

export const useExpenses = () => {
  const { user } = useAuth();
  const queryClient = useQueryClient();

  const addExpense = async (newExpense) => {
    const response = await axios.post('http://localhost:5000/expenses', {
      id: uuidv4(),
      ...newExpense,
      createdBy: user.nickname,
      userId: user.id,
    });
    return response.data;
  };

  const { mutate: addNewExpense } = useMutation(addExpense, {
    onSuccess: () => {
      queryClient.invalidateQueries(['expenses']);
    },
  });

  return {
    addNewExpense,
    // ...
  };
};

10. 지출 CRUD 리팩토링

json-server를 이용한 지출 CRUD 리팩토링

import { v4 as uuidv4 } from 'uuid';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

const API_URL = 'http://localhost:5000/expenses';

export const useExpenses = () => {
  const queryClient = useQueryClient();

  const fetchExpenses = async () => {
    const response = await axios.get(API_URL);
    return response.data;
  };

  const addExpense = async (newExpense) => {
    const response = await axios.post(API_URL, {
      id: uuidv4(),
      ...newExpense,
    });
    return response.data;
  };

  const updateExpense = async (updatedExpense) => {
    const { id } = updatedExpense;
    const response = await axios.put(`${API_URL}/${id}`, updatedExpense);
    return response.data;
  };

  const deleteExpense = async (id) => {
    await axios.delete(`${API_URL}/${id}`);
  };

  const { data: expenses, isLoading, isError } = useQuery('expenses', fetchExpenses);

  const { mutate: addNewExpense } = useMutation(addExpense, {
    onSuccess: () => {
      queryClient.invalidateQueries('expenses');
    },
  });

  const { mutate: modifyExpense } = useMutation(updateExpense, {
    onSuccess: () => {
      queryClient.invalidateQueries('expenses');
    },
  });

  const { mutate: removeExpense } = useMutation(deleteExpense, {
    onSuccess: () => {
      queryClient.invalidateQueries('expenses');
    },
  });

  return {
    expenses,
    isLoading,
    isError,
    addNewExpense,
    modifyExpense,
    removeExpense,
  };
};

axios와 react-query를 사용하여 CRUD 기능을 구현

모든 API 호출이 axios를 통해 이루어지며, 데이터 fetching, 추가, 수정, 삭제는 react-query를 통해 관리


지출 데이터 수정/삭제 시 생성한 사람만 가능하도록 설정

import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { useExpenses } from '../hooks/useExpenses';

const EditExpense = ({ expense }) => {
  const { user } = useAuth();
  const { modifyExpense } = useExpenses();
  const [formData, setFormData] = useState({ ...expense });

  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (user.id === expense.userId) {
      modifyExpense(formData);
    } else {
      alert('작성자만 수정할 수 있습니다.');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields... */}
      <button type="submit">수정</button>
    </form>
  );
};

export default EditExpense;
profile
프론트엔드 개발자를 향해서

0개의 댓글