본문 바로가기
IT/React

커스텀 훅 사용해보기

by 삐약 개발자 2025. 2. 20.

솔로프로젝트를 구현해보는중에 user 관련된 로직이 많이 중복되게 쓰이는걸 보다가 문득 커스텀훅으로 코드의 중복성을 줄여보고자 사용해봄!!

 

user 관련된 로직이 사용되는 곳

1. 로그인

2. 로그아웃

3. 헤더에서 로그인 했을때와 안했을때 상태 감지할때 user 필요

4. 마이페이지

5. 권한이 있는지 없지 확인하는 로직

 

지금 다 구현하지 않았음에도 여러곳에서 user 가 사용되고 있음을 확인할수 있음

 

user 로직을 useUser 라는 커스텀 훅을 통해 간단하게 코드를 구현해보려고 함!

 

export const useUser = () => {
  const queryClient = useQueryClient();
  // user 상태를 useQuery로 관리
  const { data: user, isLoading } = useQuery({
    queryKey: ["user"],
    queryFn: userService.getCurrentUser,
    staleTime: Infinity,
  });
  
  //로그인 요청 로직
  const loginMutation = useMutation({
    mutationFn: userService.loginUser,
    //요청이 성공하면 user 데이터를 캐시에 저장!
    onSuccess: (data) => {
      console.log("useUser = ", data);
      queryClient.setQueryData(["user"], data.user);
      //invalidateQueries => 캐시 무효롸 해서 최신 데이터 불러옴
      queryClient.invalidateQueries(["user"]);
    },
  });
  /**
   * useCallBack
   * 함수를 메모이제이션(최적화) 해서 불필요하게 생성되는것을 방지
   * 메모이제이션된 함수를 리턴함
   */
  const login = useCallback(
    (loginData) => {
      console.log(loginData);
      loginMutation.mutateAsync(loginData);
    },
    [loginMutation]
  );
  const logout = useCallback(() => {
    localStorage.removeItem("accessToken");
    localStorage.removeItem("userId");
    localStorage.removeItem("userEmail");
    delete axios.defaults.headers.common["Authorization"];
    // 로그아웃 후 user 데이터 삭제해주기
    queryClient.invalidateQueries(["user"]); 
    // user 상태를 null로 설정 => 초기화
    queryClient.setQueryData(["user"], null); 
    // 추가적으로 질문 캐시 무효화
    queryClient.invalidateQueries(["question"]); 
  }, [queryClient]);
  return { user, login, logout, isLoading };
};

 

로그인 후 사용자 정보를 React Query 캐시에 반영하는 로직

 

일단 결론적으로는 위에 코드로 커스텀 훅을 생성했음

커스텀 훅에 대한 자세한 설명과 더불어 훅에 대한 간단한 설명도 적어보려고 함

 

일단 이번에 솔로프로젝트를 하면서 리액트 쿼리를 사용해봤음

그때 useQueryClient() 훅을 정말 많이 사용했음

 

useQueryClient 는 리액트 쿼리에서 전역적으로 캐시 데이터를 관리하는 queryClient 를 가져오는 훅임

useQueryClient 를 통해 캐시된 데이터를 조회, 갱신, 삭제할수 있음

 

1. 사용자 상태관리

 const { data: user, isLoading } = useQuery({
    queryKey: ["user"],
    queryFn: userService.getCurrentUser,
    staleTime: Infinity,
  });

 

이때 사용된 useQuery 는 

 

2. 로그인 요청

const loginMutation = useMutation({
    mutationFn: userService.loginUser,
    onSuccess: (data) => {
      console.log("useUser = ", data);
      queryClient.setQueryData(["user"], data.user);
      queryClient.invalidateQueries(["user"]);
    },
});

 

3. 로그인 함수 / 로그아웃 함수 => useCallback 사용

 

  const login = useCallback(
    (loginData) => {
      console.log(loginData);
      loginMutation.mutateAsync(loginData);
    },
    [loginMutation]
  );

  const logout = useCallback(() => {
    localStorage.removeItem("accessToken");
    localStorage.removeItem("userId");
    localStorage.removeItem("userEmail");
    delete axios.defaults.headers.common["Authorization"];
    queryClient.invalidateQueries(["user"]); 
    queryClient.setQueryData(["user"], null); 
    queryClient.invalidateQueries(["question"]); 
  }, [queryClient]);

 

iuseCallback 사용하여 불필요한 함수 재생성을 방지함

loginMutation.mutateAsync(loginData) => loginMutation 을 실행하여 비동기적으로 로그인 요청을 보냄

의존성 배열에 loginMutation 넣었기에 loginMutation 이 변경되지 않는 함 login 함수는 재생성안됨

 

로그아웃 할때 로컬스토리지에 있는 사용자 정보 초기화 하며 헤더에서 인증에 필요했던 토큰 제거

 

로그인 후 사용자 정보 갱신을 위하여 invalidateQueries 사용!!!

 

이렇게 커스텀 훅을 사용했을때 원래 코드가 어떻게 달라졌는지 확인해보자

 

Header.js

const Header = () => {
  const { category } = useContext(CategoryContext);
  const queryClient = useQueryClient();
  
  const { data: user } = useQuery({
    queryKey: ["user"],
    queryFn: userService.getCurrentUser,
    staleTime: 0,
    refetchOnWindowFocus: true,
  });

  const onLogout = () => {
    localStorage.removeItem("accessToken");
    localStorage.removeItem("userId");
    localStorage.removeItem("userEmail");
    delete axios.defaults.headers.common["Authorization"];
    queryClient.invalidateQueries(["user"]);
    queryClient.invalidateQueries(["question"]);
  };
  
  return(
    <HeaderContainer>
      <div className="arrowBox">
        <ul>
          {user ? (
            <>
              	// 유저가 있을때 로직
                <li onClick={onLogout} style={{ cursor: "pointer" }}>
                    로그아웃
                </li>
            </>
          ) : (
            <>
             // 유저가 없을때 로직
            </>
          )}
        </ul>
      </div>
    </HeaderContainer>
  )

[기존 코드]

 

이를 커스텀 훅을 사용하여 아래처럼 수정함

 

const Header = () => {
  const { category } = useContext(CategoryContext);
  const { user, logout } = useUser();
  
  return(
    <HeaderContainer>
      <div className="arrowBox">
        <ul>
          {user ? (
            <>
              // 유저가 있을때 로직
                <li onClick={logout} style={{ cursor: "pointer" }}>
                  로그아웃
                </li>
            </>
          ) : (
            <>
             // 유저가 없을때 로직
            </>
          )}
        </ul>
      </div>
    </HeaderContainer>
  )

 

Login.js 로직

 

export const Login = () => {
  const navigate = useNavigate();
  const queryClient = useQueryClient();
  const loginMutation = useMutation({
    mutationFn: userService.loginUser,
    onSuccess: (data) => {
      console.log(data.user);
      queryClient.setQueryData(["user"], data.user);
      queryClient.invalidateQueries(["user"]);
      alert("로그인 성공!");
      navigate("/");
    },
    onError: () => {
      alert("로그인 실패!!!!");
    },
  });
  const { login, isLoading } = useUser();

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const data = Object.fromEntries(formData);
    loginMutation.mutate(data);
    try {
      login(data);
      alert("로그인 성공!");
      navigate("/");
    } catch (error) {
      alert("로그인 실패!!!!");
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginTop: "150px" }}>
      <input name="email" type="email" placeholder="이메일" required />
      <input name="password" type="password" placeholder="비밀번호" required />
      <button type="submit">로그인</button>
    </form>
  );
};

[기존 코드]

 

아래 또한 useUser 커스텀 훅 사용

 

export const Login = () => {
  const navigate = useNavigate();
  const { login, isLoading } = useUser();

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const data = Object.fromEntries(formData);

    try {
      login(data);
      alert("로그인 성공!");
      navigate("/");
    } catch (error) {
      alert("로그인 실패!!!!");
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginTop: "150px" }}>
      <input name="email" type="email" placeholder="이메일" required />
      <input name="password" type="password" placeholder="비밀번호" required />
      <button type="submit" disabled={isLoading}>
        {isLoading ? "로그인 중..." : "로그인"}
      </button>
    </form>
  );
};

 

 

이런식으로 전후가 차이가 크며 지금은 두군데에서만 바꿨지만 더 큰 프로젝트를 중복된 로직 없이 관리하는 법을 아는것 또한 프론트 개발자로써 꼭 가져야 하는 마음가짐이라고 생각함!!! 

'IT > React' 카테고리의 다른 글

번들링, 웹팩  (0) 2025.02.28
React - AJAX 실습(1)  (0) 2025.02.04
React - AJAX  (0) 2025.02.02
React - Hook (State, Effect)  (1) 2025.02.02
React - Side Effect  (0) 2025.02.01