24. 06. 17 TIL

이번에 수정한 코드를 총정리해보고자 한다.

 

이번엔 쿼리를 배워서 해봤는데 진짜 말도 안 되는 것이...

 

리덕스를 배울 땐 뭔 소린지 하나도 몰겠어서 미치는 줄 알았는데

 

이제 리덕스 좀 쓴다 하니까 쿼리.. (그리고 1도 모르겠음)

 

너무 헷갈려서 코드 분리도 좀 많이 해뒀고... 그래서 정리가 필요할 것 같아 쓴다.

 

먼저 기존에 있던 리덕스 슬라이스 파일을 두고 파일을 하나 더 만들었다. 

 

 

1. userSlice

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  token: localStorage.getItem('accessToken'),
  userInfo: null,
};
export const userSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    login: (state, action) => {
      state.token = action.payload;
      localStorage.setItem('accessToken', action.payload);
    },
    logout: (state) => {
      state.token = null;
      localStorage.removeItem('accessToken');
    },
    setUserInfo: (state, action) => {
      state.userInfo = action.payload;
    },
  },
});

export const { login, logout, setUserInfo, setUserImg } = userSlice.actions;
export default userSlice.reducer;

나는 이렇게 만들어줬는데

 

const initialState = {
  token: localStorage.getItem('accessToken'),
  userInfo: null,
};

 

먼저 초기값으로 token과 userInfo를 만드는데, token은 웹사이트에서 로그인할 때 필요한 문자열이고 이것을 로컬 스토리지에서 가져온다.

 

userInfo는 사용자에 대한 정보를 저장한다. 처음엔 정보가 없으니 null로 설정.

 

    login: (state, action) => {
      state.token = action.payload;
      localStorage.setItem('accessToken', action.payload);
    },

그리고 로그인 함수를 만드는데, 현재 상태를 나타내는 state, 어떤 변화가 일어날지에 대한 정보를 담고있는 action 두 가지를 인자로 받는다. 

그리고 action.payload(서버로부터 받은 토큰 값)에서 값을 가져와 state.token에 새로운 값을 넣어준다.(상태 업데이트)

그리고 그값을 로컬 스토리지에 저장한다. accessToken이라는 이름으로 새로운 토큰값(action.payload)를 저장한다.

 

    logout: (state) => {
      state.token = null;
      localStorage.removeItem('accessToken');
    },

그 다음은 로그아웃 함수인데, 로그아웃 함수는 로그아웃 할 때 새로운 정보를 받지 않으므로 state만 받는다.

로그아웃을 하면 state.token을 null로 바꾸고 로컬 스토리지에서 accessToken으로 저장된 데이터를 삭제한다.

 

    setUserInfo: (state, action) => {
      state.userInfo = action.payload;
    },

setUserInfo 함수는 사용자의 정보를 설정한다. state.userInfo에 새로운 정보를 넣고, 그 정보는 action.payload에서 가져온다.


2. 라우터

const PrivateRoute = ({ element: Element, ...rest }) => {
  const isAuthenticated = useSelector((state) => !!state.users.token);
  return isAuthenticated ? <Element {...rest} /> : <Navigate to="/login" />;
};

const PublicRoute = ({ element: Element, ...rest }) => {
  const isAuthenticated = useSelector((state) => !!state.users.token);
  return !isAuthenticated ? <Element {...rest} /> : <Navigate to="/myPage" />;
};

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/detail/:detailId" element={<Detail />} />
        <Route path="/login" element={<PublicRoute element={Login} />} />
        <Route path="/signUp" element={<PublicRoute element={SignUp} />} />
        <Route path="/myPage" element={<PrivateRoute element={MyPage} />} />
        <Route path="/signUp/myPage" element={<Navigate to="/myPage" />} />
      </Routes>
    </BrowserRouter>
  );
};
export default Router;

이렇게 되어있고,

 

const PrivateRoute = ({ element: Element, ...rest }) => {
  const isAuthenticated = useSelector((state) => !!state.users.token);
  return isAuthenticated ? <Element {...rest} /> : <Navigate to="/login" />;
};

privateRoute는 로그인한 사람만 볼 수 있는 페이지를 설정한다.

사실 이건 예시 코드를 보고 그대로 작성한 거라서... 

일단 isAuthenticated는 user.token값을 true나 false로 변환한다.

따라서 isAuthenticated는 users.token이 있으면 true, 없으면 false가 된다.


3. 서버에서 사용자 정보 가져오기.

import axios from 'axios';
import { setUserInfo } from '../store/slices/userSlice';
import { userUrl } from '../url/url';

export const fetchUserInfo = async (isAuthenticated, dispatch) => {
  try {
    const response = await axios.get(`${userUrl}/user`, {
      headers: {
        Authorization: `Bearer ${isAuthenticated}`,
      },
    });
    dispatch(setUserInfo(response.data));
    return response.data;
  } catch (error) {
    console.error('Failed to fetch user info:', error);
  }
};

이렇게 분리를 했다.

일단 fetchUserInfo는 사용자 정보를 서버에서 가져오는 함수인데,

isAuthenticated(사용자 로그인 여부에 대한 정보), dispatch(리덕스 액션 실행)을 매개변수로 받는다.

axios.get은 서버에서 데이터를 가져올 때 사용하고, 괄호 안에 사용자 정보를 가져올 서버 주소를 넣는다.

await는 서버의 응답을 기다리게 하고, 응답을 받을 때까지 다음 줄 코드를 실행하지 않는다.

 

headers는 서버에 추가 정보를 보내는 곳인데, 서버는 중요한 정보를 보호하기 위해 특정 사용자가 요청한 정보에 접근할 수 있는 권한이 있는지 확인해야 하고, 이 헤더는 서버에 이 요청이 인증된 사용자로부터 온 것임을 증명한다.

그래서 정리를 하자면, 이 코드에서는 axios.get 요청을 보낼 때 헤더를 추가하고, 이 헤더는 사용자가 받은 토큰을 포함하고 있으며, 서버는 이 토큰을 확인해서 유효한 토큰이면 사용자가 요청한 데이터를 반환해준다. 그리고 서버가 반환한 사용자 정보를 dispatch를 통해 setUserInfo에 저장한다.

 

이쯤되니 좀 헷갈린다. 다시!!

 

1. fetchUserInfo -> 서버에서 사용자 정보를 가져와 리덕스 상태에 저장한다.

2. userSlice -> 사용자 인증 상태와 정보를 관리한다.

3. privateRoute~ -> 인증 상태에 따라 사용자가 접근할 수 있는 페이지를 제어한다.


 

4. 회원 가입 페이지

const SignUpPage = () => {
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    id: '',
    password: '',
    nickname: '',
  });

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

  //서버에 보내기
  const onSubmitHandler = async (e) => {
    e.preventDefault();
    try {
      const response = await registerUser(formData);
      alert(response.message);
      navigate('/Login');
      console.log(response);
    } catch (error) {
      alert(`가입에 실패했습니다 ${error.response.data.message}`);
    }
  };

formData는 입력한 데이터를 저장하는 곳이다. id, password, nickname을 빈 문자열로 설정한다.

 

handleChange는 사용자가 입력 상자에 입력할 때마다 실행되는 함수인데,

const {name, value} = e.target은 입력 상자의 이름과 입력한 값을 가져온다.

그리고 기존의 formData에 새로운 값을 추가한다.

 

그리고 const response = await registerUser(formData)는 formData를 서버로 보내고 응답을 기다리는 코드다. 

registerUser 함수를 호출해 사용자가 입력한 회원가입 정보를 담은 formData를 보내서, 서버로부터 응답을 받고, 그 데이터를 response 변수에 저장한다.

 

그럼 registerUser 함수는?

export const registerUser = async (formData) => {
  const response = await axios.post(`${userUrl}/register`, formData);
  return response.data;
};

 

이렇게 생겼는데 이 함수는 formData를 매개변수로 받아서 서버에 회원가입 요청을 받고,

서버로부터 응답을 받으면 response 변수에 저장해서 반환한다.


5. 로그인 페이지

const LoginPage = () => {
  const [loginData, setLoginData] = useState({
    id: '',
    password: '',
  });

  const dispatch = useDispatch();
  const navigate = useNavigate();

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

  const onSubmitHandler = async (loginData) => {
    try {
      const response = await loginUser(loginData);
      if (response.success) {
        dispatch(login(response.accessToken));
        navigate('/');
      } else {
        alert('로그인에 실패하였습니다');
      }
    } catch (error) {
      alert(`오류가 발생했습니다 ${error.response.data.message}`);
    }
  };

loginData는 사용자가 입력한 로그인 정보를 저장한다.

그리고 회원가입 페이지와 비슷하게 작성하고~

로그인이 성공하면 서버 응답의 success 값이 true인데, 그렇다면 dispatch함수를 사용해서

로그인 액션을 실행하고 서버에서 받은 로그인 토큰을 상태에 저장한다.


6. 디테일 페이지

  useEffect(() => {
    if (!isAuthenticated) {
      alert('로그인한 사용자만 접근 가능합니다');
      navigate('/login');
    }
  }, [isAuthenticated, navigate]);

컴포넌트가 처음 렌더리 되거나 isAuthenticated 값이 바뀔 때 실행된다.

로그인하지 않았으면 alert를 띄우고 로그인 페이지로 이동한다.

 

  const fetchDetailById = async ({ queryKey }) => {
    const id = queryKey[1];
    const response = await axios.get(`${jsonUrl}/${id}`);
    return response.data;
  };

이 함수는 서버에서 상세 정보를 가져오는 함수다. 

querykey에서 id를 가져오고, axios.get을 사용해 서버에 요청을 보낸다.

  const {
    data: posts = [],
    isPostLoading,
    isPostError,
  } = useQuery({
    queryKey: ['posts', detailId],
    queryFn: fetchDetailById,
  });

useQuery는 데이터를 서버에서 가져오고 로딩 상태와 에러 상태를 관리한다.

여기서 쿼리 키가 posts와 detailId로 되어 있는데, 위 queryKey는 이것을 가져온 거고

queryKey[1]을 통해 detailId를 추출해 가져온 것이다. 그리고 url의 id는 detailId다.

 

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

  const editPost = async (editInput) => {
    await axios.patch(`${jsonUrl}/${editInput.id}`, editInput);
  };

그리고 수정, 삭제 요청을 보내는 함수는 이렇게 쓴다. 

특정 게시글의 id를 포함한다.

 

  const delPostMutation = useMutation({
    mutationFn: deletePost,
    onSuccess: () => {
      queryClient.invalidateQueries(['posts']);
      navigate('/');
    },
  });

이 코드는 삭제 요청을 관리하는 도구인데, deletePost 함수를 사용해 삭제 요청을 보내고,

성공하면 캐시된 데이터를 무효화하고 이를 통해 해당 데이터가 최신 상태로 유지되도록 보장하며 홈페이지로 이동한다.

 

즉, delPostMutation을 정의하고 delClickBtn에서 delPostMutation.mutate를 호출해 삭제 작업을 실행한다.

그리고 detailId를 delPostMutation.mutate에 전달한다.


7. 헤더

헤더는 로그인 했을 때 로그인한 사용자의 정보를 가져와 닉네임과 프로필 사진을 보여준다.

  const { isLoading } = useQuery({
    queryKey: ['userInfo'],
    queryFn: () => fetchUserInfo(isAuthenticated, dispatch),
    enabled: !!isAuthenticated,
    onSuccess: (data) => {
      dispatch(setUserInfo(data));
    },
  });

그렇기 때문에 이렇게 사용자 정보를 가져오면 된다. fetchUserInfo 함수가 반환한 사용자 정보를 가져온다.


8. 마이페이지 프로필, 닉네임 변경

  useEffect(() => {
    const loginData = async () => {
      if (!isAuthenticated) {
        alert('로그인이 필요합니다');
        navigate('/login');
      } else {
        await fetchUserInfo(isAuthenticated, dispatch);
      }
    };
    loginData();
  }, [isAuthenticated, navigate, dispatch]);

먼저 로그인 정보가 없으면 로그인 페이지로 이동하고, 로그인 상태면 사용자 정보를 서버에서 가져온다.

dispatch는 fetchUserInfo 함수 내부에서 호출되기 때문에 userProfile 컴포넌트에서 fetchUserInfo 함수를 호출하면서 dispatch를 인자로 전달한다.

  try {
    const updateData = await updateUser(
      userName,
      userAvatar,
      isAuthenticated
    );

그리고 updateUser 함수는 서버에 사용자 이름과 아바타를 업데이트 하는 요청을 보낸다.

그리고 인자로 userName, userAvatar, isAuthenticated를 전달한다.

    if (updateData.success) {
      const updateUserInfo = {
        ...userInfo,
        nickname: updateData.nickname || userInfo.nickname,
        avatar: updateData.avatar || userInfo.avatar,
      };
      dispatch(setUserInfo(updateUserInfo));
      alert('변경되었습니다');
      setUserName('');
      setUserAvatar(null);
    } else {
      alert('변경에 실패했습니다');
    }

그래서 서버 응답의 값이 true면 updateUserInfo 객체를 만들어서 기존 객체에 새로운 닉네임과 아바타를 포함한다.


 updatUser 함수는 이렇게 생겼다.

export const updateUser = async (userName, userAvatar, isAuthenticated) => {
  const formData = new FormData();
  formData.append('nickname', userName);
  if (userAvatar) {
    formData.append('avatar', userAvatar);
  }
  const response = await axios.patch(`${userUrl}/profile`, formData, {
    headers: {
      Authorization: `Bearer ${isAuthenticated}`,
      'Content-Type': 'multipart/form-data',
    },
  });
  return response.data;
};

사용자의 닉네임과 아바타를 업데이트 하는 함수고, 인자를 3개 받는다.

formData는 폼 데이터를 쉽게 만들 수 있게 도와주는 도구로, 이미지 파일 같은 데이터를 서버로 보낼 때 자주 사용된다.

닉네임은 formData.append를 사용해서 nickname이라는 이름으로 새로운 닉네임을 추가한다. 이것은 아바타도 마찬가지다.


8. 인풋 폼


  const addPost = async (newItem) => {
    await axios.post(`${jsonUrl}`, newItem);
  };

  const addCashMutation = useMutation({
    mutationFn: addPost,
    onSuccess: () => {
      queryClient.invalidateQueries(['posts']);
    },
  });
  
   if (dateValue && itemValue && amountValue && descriptionValue !== '') {
      addCashMutation.mutate(newItem);
      setDateValue('');
      setItemValue('');
      setAmountValue('');
      setDescriptionValue('');
    } else {
      alert('값을 모두 입력해주세요');
    }
  };

이렇게 되어있다. 다 가져온 건 아니고 일부만 가져왔다.

 

서버와 통신해 데이터를 변경하는 함수 addPost를 정의하고, useMutation 훅을 사용해 뮤테이션을 관리한다.

mutatioFn 옵션에 정의한 뮤테이션 함수(addPost)를 전달한다.

onSuccess 콜백을 통해 뮤테이션이 성공적으로 완료되었을 때 추가 작업을 정의한다.

그리고 addCashMutation.mutate(newItem)을 호출해 뮤테이셔늘 실행한다.

newItem 데이터는 뮤테이션 함수(addPost)에 전달해 서버에 데이터를 추가한다.

 

 

 

'TIL' 카테고리의 다른 글

24. 06. 21 TIL  (0) 2024.06.21
24. 06. 20 TIL  (0) 2024.06.20
axios 사용하기  (0) 2024.06.13
동기, 비동기 정리  (0) 2024.06.12
24. 06. 11 TIL 뉴스피드 프로젝트 정리  (0) 2024.06.11