Skip to content

Conversation

@Jihyun0522
Copy link

@Jihyun0522 Jihyun0522 commented Dec 11, 2025

📊 리팩토링 작업 내용

⚖️ Before vs After 비교

Before: 혼재된 로직

// ❌ 안티패턴: 모든 계층이 뒤섞임
"use client";

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

  useEffect(() => {
    // 1. Infrastructure (localStorage) -> 🌟토큰을 관리하는 로직은 tokenService로 분리
    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>;
}

After: 계층별 분리

// 1. Infrastructure Layer
export const authApi = {
  login: (data: LoginRequest) => 
    apiWrapper.post<LoginResponse>('/auth/signIn', data)
};

// 2. Domain Layer
export const authService = {
  saveTokens: (accessToken: string, refreshToken: string) => {
    tokenStorage.setAccessToken(accessToken);
    tokenStorage.setRefreshToken(refreshToken);
  }
};

// 3. Presentation Layer
export const useLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const login = async (data: LoginRequest) => {
    const response = await authApi.login(data);
    authService.saveTokens(response.accessToken, response.refreshToken);
    setUser(response.user);
  };
  
  return { login, isLoading, error };
};

// 4. UI Layer
function LoginPage() {
  const { login, isLoading, error } = useLogin();
  return <form onSubmit={handleSubmit}>...</form>;
}

🎓 적용된 설계 원칙

1. Single Responsibility Principle (SRP)

원칙: 각 모듈은 하나의 책임만 가져야 한다

적용 예시:

// ✅ Good: 각 파일이 하나의 책임만 가짐
// authApi.ts - API 호출만 담당
export const authApi = {
  login: (data) => apiWrapper.post('/auth/signIn', data)
};

// authService.ts - 비즈니스 로직만 담당
export const authService = {
  isTokenValid: (token) => { /* 검증 */ }
};

// useLogin.ts - React 상태 관리만 담당
export const useLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  // ...
};
// ❌ Bad: 여러 책임이 혼재
export const authModule = {
  login: async (data) => {
    const response = await fetch('/api/auth/login'); // API 호출
    localStorage.setItem('token', response.token);   // 저장소 접근
    if (isExpired(response.token)) return false;     // 검증 로직
    setUser(response.user);                          // 상태 관리
  }
};

2. Dependency Inversion Principle (DIP)

원칙: 상위 계층은 하위 계층의 구체적 구현이 아닌 추상화에 의존해야 한다

적용 예시:

// ✅ Good: 추상화된 인터페이스에 의존
export const useLogin = () => {
  const login = async (data: LoginRequest) => {
    // authApi의 구체적 구현은 몰라도 됨
    const response = await authApi.login(data);
    
    // authService의 내부 구현은 몰라도 됨
    authService.saveTokens(response.accessToken, response.refreshToken);
  };
};

의존성 흐름:

UI Layer (page.tsx)
    ↓ 의존
Presentation Layer (useLogin.ts)
    ↓ 의존
Domain Layer (authService.ts)
    ↓ 의존
Infrastructure Layer (authApi.ts, tokenStorage.ts)

3. Open/Closed Principle (OCP)

원칙: 확장에는 열려있고 수정에는 닫혀있어야 한다

적용 예시:

// ✅ Good: 새로운 스토리지 추가 시 기존 코드 수정 불필요
class Storage {
  get<T>(key: string): T | null { /* ... */ }
  set<T>(key: string, value: T): void { /* ... */ }
}

// 새로운 스토리지 타입 추가 (확장)
class SecureStorage extends Storage {
  get<T>(key: string): T | null {
    const encrypted = super.get(key);
    return this.decrypt(encrypted);
  }
}

4. Separation of Concerns (관심사 분리)

원칙: 서로 다른 관심사는 분리되어야 한다

적용 예시:

// ✅ Good: 각 관심사가 명확히 분리됨

// 관심사 1: API 통신
export const authApi = {
  login: (data) => apiWrapper.post('/auth/signIn', data)
};

// 관심사 2: 비즈니스 규칙
export const authService = {
  isAuthenticated: () => {
    const token = tokenStorage.getAccessToken();
    return authService.isTokenValid(token);
  }
};

// 관심사 3: UI 상태 관리
export const useLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  // ...
};

// 관심사 4: 렌더링
export default function LoginPage() {
  return <form>...</form>;
}

@Jihyun0522 Jihyun0522 self-assigned this Dec 11, 2025
Copy link
Member

@aahreum aahreum left a comment

Choose a reason for hiding this comment

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

리뷰 남겼습니다~~!!!

Comment on lines +8 to +15
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login, isLoading, error } = useLogin();

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

if (!email || !password) {
Copy link
Member

Choose a reason for hiding this comment

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

필드별로 각각 state를 작성하시는군요!
필드가 많아져도(닉네임, 이메일, 비밀번호, 비밀번호 확인...등등등.....) 각각 작성하시는 편이신가요?!

Copy link
Author

Choose a reason for hiding this comment

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

많지 않으면 필드별로 작성하려고 합니다!
로그인 페이지는 이메일, 회원가입 2개의 필드만 있어서 각 필드별로 state를 작성했는데,
회원가입 페이지처럼 3개 이상이라면 객체 형태로 묶어서 state를 관리하는 방법을 고려할 것 같습니다!

Comment on lines +5 to +21
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch {
return null;
}
}

set<T>(key: string, value: T): void {
if (typeof window === 'undefined') return;

try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('로컬스토리지 저장 에러 :', error);
}
}
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
Member

Choose a reason for hiding this comment

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

클래스 활용 잘하시는거 부럽습니다.... 저는....클래스 너무 어려워요...

Copy link
Author

Choose a reason for hiding this comment

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

사실 클래스 부분은 AI의 도움을 조금 받았습니다ㅎㅎ
Java로는 어떻게 작성하긴 했는데 TypeScript로는 조금 어려워서 결국 도움을 받았습니다...

"tabWidth": 2,
"semi": true,
"singleQuote": true,
"bracketSameLine": true,
Copy link
Member

Choose a reason for hiding this comment

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

오 지현님도 true 파신가요??!!
저도 >태그 아래로 떨어지는거 싫어해서 꼭 넣는 편입니다ㅋㅋㅋㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

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

저도 아름님처럼 똑같이 싫어서 넣었습니다ㅎㅎ

const message =
err instanceof Error ? err.message : '로그인에 실패했습니다';
setError(message);
throw err;
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
Author

Choose a reason for hiding this comment

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

useLogin 훅에서 에러를 상태로 처리한 후 다시 throw하고는 있지만, login/page.tsx의 catch 블록에서 하는 처리가 없어서 실질적으로는 에러를 무시하고 있습니다.
원래 코드를 작성할 때는 login/page.tsx의 catch 블록에서 처리하게 하려고 했는데 작성하다보니까 useLogin 훅에서 처리를 해버려서 의미없는 코드가 되어버렸어요....ㅎㅎ😓

Comment on lines +60 to +78
api.interceptors.response.use(
(response: AxiosResponse) => {
return response.data;
},
(error: AxiosError) => {
let errorMessage = '';

if (error.code === 'ECONNABORTED') {
errorMessage = '요청 시간이 초과되었습니다. 다시 시도해 주세요.';
} else if (error.request && !error.response) {
errorMessage =
'네트워크 오류가 발생했습니다. 인터넷 연결을 확인해 주세요.';
} else if (error.response?.status && error.response.status >= 500) {
errorMessage = '서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
} else if (error.response?.status === 401) {
errorMessage = '로그인이 필요합니다.';
}

if (errorMessage) toast.error(errorMessage);
Copy link
Member

Choose a reason for hiding this comment

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

에러 인터셉터가 ui로직도 함께 관리하고 있어서
이 부분도 분리해보면 좋을 것 같아요!

api.interceptors.response.use(
  (response) => response.data,
  (error: AxiosError) => {
    const status = error.response?.status;

    if (status === 401) {
      error.message = '로그인이 필요합니다.';
    } else if (status === 500) {
      error.message = '서버 오류가 발생했습니다.';
    }

    return Promise.reject(error);
  }
);

인터셉터는 에러 메세지만 던져주고
UI 레이어에서 toast.error를 관리해보는건 어떨까요?!

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신 부분은 생각해보지 못했는데 한번 시도해보겠습니다!

import { tokenStorage } from '@/shared/lib/storage/tokenStorage';

// API 베이스 URL 환경 변수
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
Copy link
Member

Choose a reason for hiding this comment

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

하나 궁금한 점이 있는데요!
더줄게 작업하실 때 서버 api 인스턴스를 별도로 만드셨나요?!

Copy link
Author

Choose a reason for hiding this comment

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

네 더줄게에서도 지금 리펙토링할 때 작성한 것처럼 별도로 작성했습니다!

Copy link
Member

Choose a reason for hiding this comment

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

역시 2개가 필요하군요... 저도 미션할때 server, client 별도로 만들어야겠어요! 감사합니다!!

if (!token) return false;

try {
const payload = JSON.parse(atob(token.split('.')[1]));
Copy link
Member

Choose a reason for hiding this comment

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

atob... 이런것도 있군요..

Copy link
Author

Choose a reason for hiding this comment

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

Base64 디코딩 함수입니다! (ASCII to Base64)
JWT의 payload가 Base64로 인코딩되어 있기 때문에, 안의 정보를 읽으려면 디코딩이 필요해서 사용했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

아하😲 친절한 설명 감사합니다!!!!

Comment on lines +9 to +22
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const checkAuth = () => {
const authenticated = authService.isAuthenticated();
setIsAuthenticated(authenticated);

if (!authenticated) {
router.push(redirectTo);
}

setIsLoading(false);
};
Copy link
Member

Choose a reason for hiding this comment

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

제가 생각했을 때는 isAuthenticated라는 state가 없어도 되지 않을까? 싶은데요!
혹시 추가하신 이유가 있으실까요?!

저는 이렇게 생각했습니다
아래 코드처럼 별도로 상태를 두지 않아도 리다이렉트를 구현할 수 있지 않을까 싶었습니다!!
혹시 제가 놓친 부분이 있다면 말씀해주세요!!!

'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { authService } from '../services/authService';

export const useAuth = (redirectTo: string = '/login') => {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const authenticated = authService.isAuthenticated();

    if (!authenticated) {
      router.push(redirectTo);
      return;
    }

    setIsLoading(false);
  }, [router, redirectTo]);

  return { isLoading };
};

Copy link
Author

Choose a reason for hiding this comment

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

말씀하신대로 없어도 되는 state입니다.
처음에 코드를 작성하면서 혹시 나중에 사용할까 싶어서 추가한 state인데 결국에는 사용하지 않아서 불필요한 state가 되어버렸습니다...
제가 지웠어야 했는데 미쳐 확인을 못하고 그냥 남겨 두었어요ㅎㅎ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants