Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// ❌ 안티패턴: 모든 계층이 뒤섞임
"use client";

function ProfilePage() {
const [user, setUser] = useState(null);

useEffect(() => {
// 1. Infrastructure (localStorage)
const token = localStorage.getItem('accessToken');

// 2. Domain Logic (토큰 검증)
if (!token || isTokenExpired(token)) {
router.push('/login');
return;
}
// 3. Infrastructure (fetch)
fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
})
// 4. Presentation Logic
.then(res => res.json())
.then(data => setUser(data));
}, []);

return <div>{user?.name}</div>;
}


// ♻️ Refactor

// 기존에는 ProfilePage 안에서 토큰 확인 + fetch + 리다이렉트를 모두 처리하고있음

// services/storage.ts
// 저장소 역할만 담당
// 토큰을 어디서 읽는지만 담당

// 추후에 뭔가 변화가 생긴다면 모든 페이지에서 수정하는 것이 아닌 이곳에서만 수정하면
// 한번에 해결됨
export const storage = {
getToken: () => localStorage.getItem('accessToken'),
};
Comment on lines +40 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 앱라우터 환경에서 사용하는거라고 가정한다면
window 타입 체크해도 좋을 것 같아요!

export const storage = {
  getToken() {
    if (typeof window === "undefined") return null;
    return localStorage.getItem("accessToken");
  }
};

아니면 getToken을 사용하는 곳에서는 'use Client'를 적어야 한다고 명시해도 좋구요!


// services/api.ts
// 서버와 통신만을 책임짐
// 백엔드와 통신만을 담당

// Pages, Hooks, Components 어디서든 API 로직 재사용 가능
// 에러 핸들링 일원화 가능
// 추후에 axios 같은 라이브러리로 변경시 여기에서만 바꿔주면 됨
export const api = {
getUser: async (token: string) => {
const res = await fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
};
Comment on lines +39 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체 내부에 함수 선언하는 방식을 선호하시나요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그런건 없고 필요에 따라 사용하는 것 같아요!


// hooks/useUser.ts
// 유저 정보를 react-query로 관리
// 토큰을 기반으로 유저 정보 가져오는 로직만 담당

// 이제 페이지에서 fetch 관리 안 해도됨
import { useQuery } from '@tanstack/react-query';

export function useUser() {
const token = storage.getToken();

return useQuery({
queryKey: ['user', token],
queryFn: () => api.getUser(token!),
enabled: !!token && !isTokenExpired(token),
});
Comment on lines +70 to +74
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리액트 쿼리는 이렇게 사용하는거군요...😲

}

// components/ProtectedRoute.tsx
// 공통 라우트 보호 처리
// 토큰 체크 후 보호 라우트 처리만 담당

// 모든 페이지에 같은 로그인 체크 로직을 넣을 필요가 없음
// 재사용 가능
export function ProtectedRoute({ children }) {
const router = useRouter();
const token = storage.getToken();

useEffect(() => {
if (!token || isTokenExpired(token)) {
router.push('/login');
}
}, [token]);

if (!token) return null;
return children;
}

Comment on lines +83 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이렇게 Route로 하나 더 분리해볼 수도 있겠네요!!!
저는 이 부분을 생각을 못했던 것 같아요...🫠

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 저도 gpt의 도움을 받아서 알게된 사실이에요!

// ProfilePage.tsx
// 페이지 본연의 역할만 담당
// 렌더만 담당

// 읽기 쉬워짐, 유지보수 쉬워짐, 테스트가 쉬워짐
// 난잡하게 로직이 섞여져있던 형태가 사라짐
"use client";
export function ProfilePage() {
const { data: user } = useUser();

return (
<ProtectedRoute>
<div>{user?.name}</div>
</ProtectedRoute>
);
}