[HOTSPOT] 2026.05.29 - 주요 작업 요약

2026. 5. 29. 20:33프로젝트/HOTSPOT

1) 오늘의 작업 요약

오늘은 작업을 하면서 꽤 자주 헷갈렸다. 단순히 도메인 문자열만 바꾸면 끝날 것 같았는데, 백엔드 CORS 설정, 프론트 API 호출 주소, OBS 위젯 렌더링, 팔로우 상태 조회, 관리자 화면의 구매 내역까지 여러 지점이 서로 연결되어 있었다. 특히 도메인 마이그레이션은 겉으로는 URL 변경이지만, 실제로는 서비스가 외부에서 어떻게 호출되고 어떤 출처를 허용할지 다시 점검하는 작업이라는 것을 느꼈다.

  • 서비스 도메인을 기존 주소에서 hotspotlive.online 중심으로 정리했다.
  • 백엔드 CORS 설정에서 불필요한 localhost 허용을 제거했다.
  • 지원하지 않는 HTTP 메서드 요청이 500이 아닌 405로 응답되도록 예외 처리를 보완했다.
  • 카테고리 삭제 시 연관 데이터 문제로 터지지 않도록 안전성을 강화했다.
  • 팔로우 버튼 표시 조건과 팔로워/팔로잉 이동 흐름을 수정했다.
  • 팔로우 상태를 조회할 수 있도록 프론트 API에 상태 조회 함수를 추가했다.
  • 매니저 대시보드에 내 구매 내역 탭을 추가했다.
  • 상품 등록 숫자 입력에서 0이 의도치 않게 지워지는 문제를 수정했다.
  • OBS 위젯의 더미데이터를 제거하고 실제 데이터 렌더링과 크기 처리를 정리했다.
  • 티스토리 개발 일지 발행을 위한 자동 블로그 CLI 스크립트를 추가했다.

2) 주요 변경사항 상세

2-1. 도메인 마이그레이션과 CORS 정리

가장 먼저 진행한 작업은 서비스 도메인을 hotspotlive.online 기준으로 정리하는 것이었다. 프론트엔드와 백엔드 양쪽에 남아 있던 기존 도메인을 변경했고, 운영 환경에서 더 이상 필요하지 않은 localhost CORS 허용도 제거했다.

CORS는 브라우저가 다른 출처의 리소스를 요청할 때 서버가 해당 출처를 허용하는지 확인하는 보안 정책이다. 서버가 Access-Control-Allow-Origin을 통해 허용한 출처가 아니면 브라우저가 응답을 차단한다. 그래서 운영 서비스에서는 개발용 localhost를 계속 열어두는 것이 바람직하지 않다.

@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOrigins("https://hotspotlive.online")
                        .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                        .allowedHeaders("*")
                        .allowCredentials(true);
            }
        };
    }
}

이번 작업을 하면서 느낀 점은 도메인 변경은 단순 치환이 아니라는 것이다. API 서버, 프론트 배포 주소, 인증 쿠키, CORS, 외부 위젯 호출 주소까지 함께 봐야 실제 운영에서 문제가 줄어든다.

2-2. 잘못된 HTTP 메서드 요청을 405로 처리

기존에는 지원하지 않는 HTTP 메서드로 요청했을 때 서버 내부 오류처럼 500이 내려갈 수 있었다. 하지만 클라이언트가 잘못된 메서드로 요청한 경우에는 서버 장애가 아니라 405 Method Not Allowed가 더 정확하다.

문제 → 지원하지 않는 메서드 요청이 500으로 응답될 수 있었다.

원인 → HttpRequestMethodNotSupportedException에 대한 전역 예외 처리가 명확하지 않았다.

해결 → 해당 예외를 잡아 405 상태 코드와 일관된 에러 응답을 반환하도록 처리했다.

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ErrorResponse> handleMethodNotSupported(
        HttpRequestMethodNotSupportedException e
) {
    ErrorResponse response = new ErrorResponse(
            405,
            "지원하지 않는 HTTP 메서드입니다."
    );

    return ResponseEntity
            .status(HttpStatus.METHOD_NOT_ALLOWED)
            .body(response);
}

HTTP 상태 코드는 클라이언트와 서버가 문제의 성격을 합의하는 약속이다. 500은 서버 내부 오류, 405는 경로는 존재하지만 해당 메서드는 허용되지 않는 상황이다. 상태 코드를 정확히 맞추면 프론트에서도 에러 처리 분기가 훨씬 명확해진다.

2-3. 카테고리 삭제 안전성 강화

카테고리 삭제는 생각보다 조심해야 하는 작업이었다. 카테고리 자체만 삭제하면 될 것 같지만, 해당 카테고리를 참조하는 게시글이나 상품, 다른 연관 데이터가 있을 수 있다. 이 상태에서 무작정 삭제하면 외래키 제약 조건이나 참조 무결성 문제가 발생할 수 있다.

@Transactional
public void deleteCategory(Long categoryId) {
    Category category = categoryRepository.findById(categoryId)
            .orElseThrow(() -> new IllegalArgumentException("카테고리를 찾을 수 없습니다."));

    boolean used = productRepository.existsByCategoryId(categoryId);
    if (used) {
        throw new IllegalStateException("사용 중인 카테고리는 삭제할 수 없습니다.");
    }

    categoryRepository.delete(category);
}

이번에는 삭제 전에 사용 여부를 확인하는 방향으로 안전성을 높였다. 삭제 기능은 성공 케이스보다 실패 케이스를 더 꼼꼼히 봐야 한다는 점을 다시 느꼈다.

2-4. 팔로우 버튼과 팔로우 상태 조회 개선

프론트에서는 비소유자 프로필에서 팔로우 버튼이 보여야 하는데 조건이 어긋나 있었다. 내 프로필인지, 다른 사용자의 프로필인지, 이미 팔로우 중인지에 따라 버튼 표시와 동작이 달라져야 한다.
이를 위해 팔로우 상태를 조회하는 API 함수를 추가했다. 화면은 단순히 버튼 하나를 보여주는 것처럼 보이지만, 실제로는 현재 로그인 사용자와 프로필 주인 사이의 관계 상태를 먼저 알아야 한다.

export const followApi = {
  getStatus: async (targetUserId) => {
    const response = await api.get(`/follows/status/${targetUserId}`);
    return response.data;
  },

  follow: async (targetUserId) => {
    const response = await api.post(`/follows/${targetUserId}`);
    return response.data;
  },

  unfollow: async (targetUserId) => {
    const response = await api.delete(`/follows/${targetUserId}`);
    return response.data;
  }
};

팔로워/팔로잉 목록으로 이동하는 네비게이션도 함께 수정했다. 프로필 화면은 사용자 관계 기능의 중심이라서, 버튼 표시 조건과 이동 경로가 조금만 틀어져도 사용자가 기능이 없다고 느낄 수 있다.

2-5. 매니저 대시보드 구매 내역 탭 추가

매니저 대시보드에는 운영자가 확인해야 할 정보가 점점 늘어나고 있다. 오늘은 그중 내 구매 내역을 별도 탭으로 추가했다. 탭 구조를 사용하면 한 화면 안에서 관련 기능을 묶되, 사용자가 필요한 정보만 빠르게 볼 수 있다.

const tabs = [
  { key: "products", label: "상품 관리" },
  { key: "orders", label: "주문 관리" },
  { key: "purchases", label: "내 구매 내역" }
];

대시보드 기능을 추가할 때는 단순히 데이터를 보여주는 것보다, 사용자가 어떤 맥락에서 이 정보를 찾는지를 먼저 생각해야 한다. 구매 내역은 관리 기능이라기보다 개인 활동 이력에 가까워서 기존 관리 탭과 구분되도록 구성했다.

2-6. 상품 등록 숫자 입력에서 0이 지워지는 문제 수정

상품 등록 화면에서는 숫자 입력 중 0이 지워지는 문제가 있었다. 이 문제는 숫자 입력값을 상태로 관리할 때 자주 발생한다. 사용자가 입력 중인 값은 문자열인데, 코드에서 너무 빨리 숫자로 변환하면 0, 빈 문자열, 입력 중간 상태를 구분하기 어려워진다.

문제 → 사용자가 숫자 필드에 0을 입력하면 값이 사라졌다.

원인 → 입력값을 즉시 숫자로 변환하거나 falsy 값으로 처리하면서 0이 빈 값처럼 취급되었다.

해결 → 입력 중에는 문자열 상태를 유지하고, 제출 또는 검증 시점에 숫자로 변환하도록 정리했다.

const [price, setPrice] = useState("0");

const handlePriceChange = (e) => {
  const value = e.target.value;

  if (/^\d*$/.test(value)) {
    setPrice(value);
  }
};

const submit = () => {
  const payload = {
    price: Number(price || 0)
  };

  return productApi.create(payload);
};

입력 UI에서는 사용자가 입력하는 중간 상태를 존중해야 한다. 숫자 필드라고 해서 항상 상태를 number로만 들고 가는 것이 정답은 아니었다.

2-7. OBS 위젯 더미데이터 제거와 실제 렌더링 수정

OBS 위젯에서는 더미데이터가 남아 있어 실제 방송 환경에서 데이터가 정확히 보이지 않을 수 있었다. 더미데이터를 제거하고 실제 API 응답 기준으로 렌더링되도록 수정했다. 또한 OBS 브라우저 소스 특성상 화면 크기와 투명 배경, 고정된 레이아웃이 중요해서 크기 처리도 함께 정리했다.

useEffect(() => {
  const fetchWidgetData = async () => {
    const data = await widgetApi.getLiveOverlay(liveId);
    setWidgetData(data);
  };

  fetchWidgetData();
}, [liveId]);

OBS 위젯은 일반 웹 페이지와 다르게 사용자가 직접 스크롤하거나 새로고침하면서 확인하지 않는다. 그래서 최초 렌더링, 데이터 없음 상태, 크기 고정, 투명 배경 처리가 더 중요하다.

2-8. 개발 일지 자동 발행 CLI 추가

마지막으로 티스토리 개발 일지 발행을 자동화하기 위한 CLI 스크립트를 추가했다. 매번 커밋 내역을 정리하고 글 형식에 맞춰 작성하는 과정은 반복 작업이 많다. 그래서 CLI를 통해 개발 일지 작성과 발행 흐름을 줄일 수 있도록 했다.

#!/usr/bin/env node

async function main() {
  const title = process.argv[2];
  const contentPath = process.argv[3];

  if (!title || !contentPath) {
    throw new Error("제목과 본문 파일 경로가 필요합니다.");
  }

  // Tistory API 호출 로직
  await publishPost({ title, contentPath });
}

main().catch((error) => {
  console.error(error.message);
  process.exit(1);
});

이 작업은 개발 기록을 꾸준히 남기기 위한 기반 작업이다. 자동화는 귀찮음을 없애는 목적도 있지만, 같은 형식의 기록을 안정적으로 쌓게 해준다는 점에서 의미가 있다.

3) 기술 메모/배운 점

Q&A. 500과 405는 어떻게 다를까?

  • Q. 지원하지 않는 HTTP 메서드 요청은 서버 오류일까?
  • A. 아니다. 서버가 죽거나 로직이 실패한 것이 아니라, 클라이언트가 해당 경로에서 허용하지 않는 메서드를 사용한 것이다.
  • Q. 그래서 어떤 상태 코드가 맞을까?
  • A. 405 Method Not Allowed가 더 적절하다.
  • Q. 왜 상태 코드를 정확히 나눠야 할까?
  • A. 프론트엔드, 로그 모니터링, 운영 대응에서 원인을 더 빠르게 구분할 수 있기 때문이다.

Q&A. CORS와 도메인 변경은 왜 같이 봐야 할까?

  • Q. 도메인만 바꾸면 API 호출이 자동으로 될까?
  • A. 아니다. 브라우저는 출처가 바뀌면 CORS 정책을 다시 검사한다.
  • Q. localhost를 운영 CORS에 남겨두면 왜 좋지 않을까?
  • A. 운영 서버가 개발용 출처까지 허용하게 되어 불필요한 접근 면이 생긴다.
  • Q. 운영에서는 어떤 방식이 좋을까?
  • A. 실제 서비스 도메인만 명시적으로 허용하는 편이 안전하다.

Q&A. 숫자 입력값은 number로 관리하는 게 항상 좋을까?

  • Q. 가격 입력 필드는 숫자니까 상태도 number여야 할까?
  • A. 꼭 그렇지는 않다. 입력 중에는 빈 문자열, 앞자리 0, 아직 완성되지 않은 값이 존재할 수 있다.
  • Q. 그럼 언제 number로 바꾸는 게 좋을까?
  • A. 제출하거나 검증하는 시점에 변환하는 편이 UI 버그를 줄이기 쉽다.

오늘은 기능 추가보다 연결 지점을 정리하는 일이 많았다. 도메인 변경, CORS, 예외 처리, 입력 상태, 실제 데이터 렌더링처럼 사용자는 잘 의식하지 못하지만 서비스 안정성에는 중요한 부분들을 다듬었다. 특히 작은 상태 코드 하나, 입력값 처리 하나가 프론트와 백엔드 전체 경험에 영향을 줄 수 있다는 점을 다시 복습한 하루였다.