실제 운영 중인 웹 서비스의 특정 페이지에서 이상한 점을 발견했다.

어떤 페이지는 빠르게 로딩되고, 어떤 페이지는 눈에 띄게 느렸다.

사용자들도 "왜 이 페이지만 느려요?"라는 문의를 자주 했다. 무엇이 문제인지 찾아보기로 했다.


문제 발견

Chrome DevTools의 Network 탭으로 각 페이지의 API 호출 패턴을 확인했다.

예시

  1. F12로 개발자 도구 열기
  2. Network 탭에서 페이지 새로고침 후 XHR/Fetch 필터 적용
  3. 사용자 정보 관련 API 호출 패턴 확인
  4. 각 페이지별 호출 횟수와 데이터 크기 측정

결과는 다음과 같았다.

/관리자 페이지 A 0회 0 kB
/관리자 페이지 B 1회 1.1 kB
/관리자 페이지 C 1회 1.1 kB
/관리자 페이지 D 2회 2.2 kB

같은 사용자 정보 관련 API를 페이지마다 다른 횟수로 호출하고 있었다.


성능 측정

실제 사용자가 체감하는 성능 차이를 확인하기 위해 Lighthouse로 측정해봤다.

프로덕션 환경에서 측정한 결과는 다음과 같았다.

 

관리자 페이지 D  페이지 (API 2회 호출)

  • Performance Score: 20/100점 (Poor)

관리자 페이지 A  페이지 (API 0회 호출)

  • Performance Score: 69/100점 (Needs Improvement)

차이가 49점이었다. 245%의 성능 격차가 났다.


원인 분석

코드를 확인해보니 각 페이지마다 사용자 정보를 가져오는 방식이 달랐다.

최적화된 페이지는 이미 로딩된 데이터를 재사용했다.

// 예시 : 최적화된 페이지
const TeachersPage = () => {
  const [userInfo] = useAtom(userAtom);
  
  if (!checkUserData(userInfo)) {
    return <LoadingSpinner />;
  }
  
  return <TeachersTable />;
};

 

문제가 있는 페이지는 컴포넌트에서 독립적으로 API를 호출했다.

// 예시 : 문제가 있는 페이지
const DocumentHistoryPage = () => {
  const [userInfo] = useAtom(userAtom);
  
  useEffect(() => {
    fetchUserProfile(); // 첫 번째 호출
  }, []);
  
  useEffect(() => {
    if (someCondition) {
      fetchUserProfile(); // 두 번째 호출
    }
  }, [dependency]);
  
  return <HistoryTable />;
};

해결 방법

Layout 레벨에서 한 번만 API를 호출하고, 모든 페이지에서 캐시된 데이터를 사용하도록 변경했다.

// 예시 : _layout.jsx
const Layout = ({ children }) => {
  const [userInfo, setUserInfo] = useAtom(userAtom);
  
  useEffect(() => {
    if (!userInfo) {
      fetchUserProfile().then(setUserInfo);
    }
  }, []);
  
  return (
    <div>
      <Header />
      {children}
      <Footer />
    </div>
  );
};

모든 페이지에서 일관된 패턴을 사용하도록 수정했다.

const OptimizedPage = () => {
  const [userInfo] = useAtom(userAtom);
  
  if (!checkUserData(userInfo)) {
    return <LoadingSpinner />;
  }
  
  return <PageContent />;
};

기존에 각 페이지에서 개별적으로 호출하던 fetchUserProfile() 함수 호출을 제거했다.


개선 결과

Lighthouse 점수 20점 71점 3.55배 향상
API 호출 횟수 2회 0회 개선
데이터 전송량 2.2 kB 0 kB 개선
성능 등급 Poor Needs Improvement 2단계 상승

Network 탭으로 확인한 결과, 관리자 페이지 D에서 사용자 정보 API 호출이 완전히 제거되었다.

 


배운 점

  • 추측이나, 감이 아닌 Chrome DevTools와 Lighthouse 같은 도구로 성능에 대한 객관적인 데이터를 얻는것이 중요하다.
  • 같은 팀에서 개발해도 페이지마다 다른 패턴이 적용될 수 있다는 점을 확인했다. 정기적인 코드 리뷰와 아키텍처 가이드라인이 필요하다.
  • 단순해 보이는 API 중복 호출 제거로 성능 향상을 시킬 수 있다.

Claude Code는 Anthropic에서 개발한 agentic 코딩 도구로, 터미널에서 자연어로 코딩 작업을 수행할 수 있게 해 준다.

원래는 Max 구독($100/월) 사용 가능했지만, 최근 Pro 구독($20/월)에서도 사용할 수 있게 되어 설치를 해보았다. 

 

Windows에서 직접 설치하는 방법과 발생한 문제를 해결한 방법에 대해 공유하고자 한다.


1. 설치 환경 확인

Claude Code는 macOS와 Linux에서 공식 지원되며, Windows에서는 WSL2를 통해서만 사용할 수 있다.

먼저 Windows에서 직접 설치를 시도했다.

npm install -g @anthropic-ai/claude-code

 

cmd에서 위 명령어 실행 시 다음과 같이 에러가 발생했다

npm error Error: Claude Code is not supported on Windows.
npm error Claude Code requires macOS or Linux to run properly.

 

 이 에러를 통해 WSL2 환경 구축이 필요함을 확인했다.


 

2. WSL2 설치

Windows에서 Linux 환경을 사용하기 위해 WSL2를 설치했다.

PowerShell을 관리자 권한으로 실행하여 다음 명령어를 입력했다.

wsl --list

 

실행 시 사용법만 표시되고 설치된 배포판이 없었다.

 

해결 방법

PowerShell을 관리자 권한으로 실행하여 Ubuntu를 설치했다.

wsl --install -d Ubuntu-22.04
설치 중: 가상 머신 플랫폼
가상 머신 플랫폼이(가) 설치되었습니다.
설치 중: Linux용 Windows 하위 시스템
Linux용 Windows 하위 시스템이(가) 설치되었습니다.
설치 중: Ubuntu 22.04 LTS
Ubuntu 22.04 LTS이(가) 설치되었습니다.
요청한 작업이 잘 실행되었습니다. 시스템을 다시 시작하면 변경 사항이 적용됩니다.

 

설치 후 시스템 재시작이 필요했다.


3. 시스템을 재시작한 후 WSL2 실행

wsl --list --verbose

 

Linux용 Windows 하위 시스템 설치된 배포가 없습니다.

 

재시작 후에도 배포판이 인식되지 않았다. WSL 상태를 확인한 결과, 가상 머신 플랫폼이 제대로 활성화되지 않은 것을 발견했다.

 

해결 방법

wsl --status

WSL2는 현재 컴퓨터 구성에서 지원되지 않습니다.
"가상 머신 플랫폼" 선택적 구성 요소를 사용하도록 설정하고 BIOS에서 가상화가 사용하도록 설정되어 있는지 확인하세요.
실행하여 "가상 머신 플랫폼"을 사용하도록 설정: wsl.exe --install --no-distribution

 

에러 메시지에서 제시한 해결 방법을 적용했다.

wsl.exe --install --no-distribution

기능을 사용하도록 설정하는 중
[==========================100.0%==========================] 
The operation completed successfully.
요청한 작업이 잘 실행되었습니다. 시스템을 다시 시작하면 변경 사항이 적용됩니다.

 

다시 시스템을 재시작한 후 WSL2가 정상적으로 작동하는 것을 확인했다.


 

4. Ubuntu 초기 설정

재시작 후 Ubuntu가 정상적으로 설치되었고, 초기 사용자 설정을 진행했다

Ubuntu 22.04 LTS 시작하는 중...
Installing, this may take a few minutes...
Please create a default UNIX user account. The username does not need to match your Windows username.
Enter new UNIX username: [이름]
Enter new UNIX password: [비밀번호]
Retype new UNIX password: [비밀번호 확인]
[이름]@DESKTOP-NJF3F27:~$

 

설정이 완료되면 Ubuntu 터미널 프롬프트가 표시된다.


5. Node.js 및 Claude Code 설치

Claude Code 설치를 위해 Node.js를 설치했다.

 

Node.js 설치

sudo apt update
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version  # v20.19.2
npm --version   # 10.8.2

 

설치 완료 후 버전을 확인했다.

 

Claude Code 설치

npm install -g @anthropic-ai/claude-code

 

Claude Code 설치 시 권한 에러가 발생했다.

npm install -g @anthropic-ai/claude-code
npm error Error: EACCES: permission denied, mkdir '/usr/lib/node_modules/@anthropic-ai'

 

sudo 권한으로 재시도했다.

sudo npm install -g @anthropic-ai/claude-code

 

설치가 성공적으로 완료되었다.

 


6. Claude Code 인증

Claude Code를 실행하여 인증을 진행했다.

claude

 

WSL2 환경에서는 브라우저가 자동으로 열리지 않아 인증 URL이 터미널에 표시되었다.

Browser didn't open? Use the url below to sign in:
https://claude.ai/oauth/authorize?code=true&client_id=...

 

제공된 URL을 Windows 브라우저에서 직접 열어 Claude Pro 계정으로 로그인하고, 인증 코드를 터미널에 입력했다.


7. 보안 확인 단계

인증 완료 후 보안 주의사항이 나타났다. Enter 키를 눌러 계속 진행했다.

Security notes:
1. Claude can make mistakes
2. Due to prompt injection risks, only use it with code you trust

 

Claude Code 실행 시 작업 폴더에 대한 신뢰 확인이 표시되었다. 1. Yes, proceed 를 선택하여 진행했다.

Do you trust the files in this folder?
/home/[이름]

 

 


8. IntelliJ 연동 설정

Windows PowerShell에서는 claude 명령어가 인식되지 않아, IntelliJ 터미널을 WSL2 환경으로 변경하여 해결했다.

IntelliJ 설정을 다음과 같이 변경했다

File → Settings → Tools → Terminal
Shell path: wsl.exe
Working directory: (비워두기)

 

이후 IntelliJ를 재시작하여 터미널이 자동으로 WSL2 환경에서 열리도록 설정했다.


9. 프로젝트 폴더 접근

WSL2에서 Windows 프로젝트 폴더에 접근할 때 경로 문제가 발생했다.

 

해결 방법

WSL2에서 Windows 프로젝트 폴더에 접근할 때는 마운트 경로를 사용해야 한다.

# Windows 경로: C:\dev\workspace\projectName\projectName-admin
# WSL2 경로: /mnt/c/dev/workspace/projectName/projectName-admin

cd "/mnt/c/dev/workspace/projectName/projectName-admin"

 

Claude Code는 보안상 원래 작업 디렉토리 외부로의 이동을 제한한다.

프로젝트 폴더 접근 시 다음과 같은 확인 메시지가 나타났다.

ERROR: cd to '/mnt/c/dev/workspace/projectName/projectName-admin' was blocked. 
For security, Claude Code may only change directories to child directories of the original working directory (/home/[이름]) for this session.

Do you want to proceed?
1. Yes
2. Yes, and add /mnt/c/dev/workspace/projectName/projectName-admin as a working directory for this session
3. No, and tell Claude what to do differently

 

2번 옵션을 선택하여 해당 디렉토리를 신뢰할 수 있는 작업 디렉토리로 추가했다.

 


10. 최종 확인

설정 완료 후 Claude Code가 프로젝트를 정상적으로 분석하는 것을 확인했다.

This looks like a Spring Boot Java web application for the ProjectName admin panel. 
It has a typical Maven/Gradle project structure with source code, resources, templates, 
and configuration files. The application appears to be for managing course data, 
users, events, coupons, and other administrative functions.

11. 개발 지침 설정

프로젝트 루트에 CLAUDE.md 파일을 생성하여 개발 규칙과 가이드라인을 설정했다. 이 파일에는 다음과 같은 내용을 포함했다.

 

  • Spring Boot 개발 패턴
  • 코딩 컨벤션
  • 신입 개발자를 위한 용어 설명
  • 실무 중심의 개발 가이드라인
  • 보안 및 성능 최적화 규칙

결론

  • Windows 환경에서 Claude Code를 사용하려면 WSL2를 통한 Linux 환경 구축이 필수다. 
  • Claude Code를 Pro 버전으로 사용할 수 있다는 점이 큰 장점인 거 같다. 
  • 아직 신입 개발자이기 때문에 Claude Code에 의존하기 보다는, 경험 많은 시니어 개발자에게 조언을 구하는 느낌으로 활용하는 것이 좋을 것 같다고 생각한다.

1. 배경

사용자 개인정보 보호를 위해 전화번호와 이메일을 데이터베이스에 AES로 암호화해 저장했다. 

마이페이지에서 사용자의 전화번호와 이메일을 복호화하여 보여주기 위해 MyBatis 쿼리에 복호화 함수를 적용했다.


2. 문제

복호화 함수를 적용했음에도 조회 결과가 VO에 매핑되지 않아 화면에는 암호화된 값이 그대로 노출되었다.

복호화 함수 자체는 DB에서 정상 작동하고 있었지만, API 응답에서는 값이 null로 내려왔다.

 

오류 발생한 쿼리

<select id="findUser" resultType="UserResponseVo">
    SELECT
      user_id,
      FN_DECRYPT_AES(phone_number),
      FN_DECRYPT_AES(email_address)
    FROM user__user
    WHERE user_id = #{userId}
</select>

 

VO

public class UserResponseVO {
	private String phoneNumber;
    private String emailAddress;
}

 

MyBatis에서 resultType은 쿼리의 컬럼명과 VO의 필드명이 정확히 일치해야 자동 매핑이 된다.

FN_DECRYPT_AES(phone_number)처럼 함수 호출 결과는 별칭(alias)이 없으면 함수 호출 문자열이 그대로 컬럼명으로 내려간다.

따라서 VO의 phoneNumber와 일치하지 않아 매핑되지 않았다.


3. 해결 방향

함수 결과에 별칭(alias)을 붙여 컬럼명과 필드명을 일치시킨다.

그 결과 복호화된 전화번호와 이메일이 정상적으로 VO에 매핑되었고 화면에도 복호화된 값이 정확히 출력되었다.

<select id="findUser" resultType="UserResponseVo">
    SELECT
      user_id,
      FN_DECRYPT_AES(phone_number)  AS phoneNumber,
      FN_DECRYPT_AES(email_address) AS emailAddress
    FROM user__user
    WHERE user_id = #{userId}
</select>

4. 회고

함수 호출이나 연산 컬럼은 반드시 별칭을 붙여야 한다는 MyBatis의 기본 원칙을 다시 확인할 수 있었다.

쿼리에서 별칭을 누락하면 복호화 함수가 잘 동작하고 있어도 매핑이 되지 않아 디버깅에 불필요한 시간을 쓸 수 있다.

1. 배경

일간 활성 사용자(DAU)와 월간 활성 사용자(MAU)를 수집해야 하는 요구사항이 있었다.
문제는 시스템이 오래되었고, 로그인 기록 테이블과 사용자 테이블 간의 키가 정규화되어 있지 않았다.

로그인 기록 테이블(login_activity)의 member_id와 사용자 테이블(member)의 email을 매칭해야 했다.
즉, 인덱스로 보장되지 않는 조건으로 조인을 수행해야 했다.


2. 기존 쿼리의 문제

활성 사용자 통계는 단일 테이블로는 수치가 정확하지 않았다.
탈퇴 사용자나 휴면 계정까지 포함될 수 있기 때문이다.

-- 부정확한 DAU 예시
SELECT COUNT(DISTINCT member_id)
FROM login_activity
WHERE login_date = '2025-06-17';

따라서 사용자 상태(is_active, is_deleted)를 반영하려면 반드시 사용자 테이블과 조인이 필요했다.
그러나 member_id와 email은 FK로 관리되고 있지 않아 조인 성능은 매우 떨어졌다.


3. 해결 방향

기존 아키텍처를 변경하는 것은 불가능했기에, 다음 세 가지를 최우선 과제로 삼았다.

  • 정확한 통계 수치를 확보하는 것
  • 외래키(FK) 구조 개선 계획은 별도로 상사와 논의하여 추진할 것
  • 서비스에 미치는 영향을 최소화하는 것

추가로, 정상적인 통계 조회에 실패할 경우를 대비해 Fallback 처리를 적용했다.
조회 실패 시 통계 수치는 0으로 대체하여, 서비스 장애가 발생하지 않도록 설계했다.


4. 최종 쿼리와 로직 (예시)

조인 쿼리 (예시)

-- DAU: 조인 기반 정확 통계
SELECT COUNT(DISTINCT L.member_id)
FROM login_activity L
JOIN member M ON L.member_id = M.email
WHERE DATE(L.login_date) = #{targetDate}
  AND M.is_deleted = 0
  AND M.is_active = 1;

 

서비스 로직 (예시)

private Integer getDailyActiveUsers(LocalDate targetDate) {
    try {
        Integer count = memberStatisticsMapper.countDailyActiveUsers(targetDate);
        return count != null ? count : 0;
    } catch (Exception e) {
        log.warn("DAU fallback: {}", e.getMessage());
        return 0;
    }
}

 


 

5. 결과

  • 데이터 정확성 확보
    탈퇴 및 비활성 계정을 제외한 정확한 DAU/MAU 집계가 가능해졌다.
  • 성능 이슈 감내
    인덱스를 타지 않는 문제는 구조적 한계로 남았다.
    트래픽 규모를 고려할 때 허용 가능한 수준으로 판단했다.
  • Fallback 안전성
    쿼리가 실패하더라도 서비스 오류가 발생하지 않도록 보호 장치를 마련했다.

6. 회고

레거시 시스템에서 데이터 설계가 완벽하지 않은 경우가 꽤 많다.

이번 사례를 겪으면서 조인을 완전히 피할 수 없을 때 어떻게 절충할지 배울 수 있었다.

앞으로는 이런 부분들을 개선해야 할 필요가 있다.

  • login_activity.member_id를 FK로 정비하는 작업
  • DATE() 함수 대신 범위 조건을 활용해 인덱스 효과 극대화
  • Redis 같은 캐시 도입으로 쿼리 부하 분산

이런 작업들이 진행되면 훨씬 더 안정적이고 효율적인 통계 서비스가 가능해질 것이다.

 

React 프로젝트에서 특정 컴포넌트를 살펴보면 component.jsx 파일과 index.js 파일이 함께 존재하는 경우가 많습니다. 이 두 파일의 역할과 함께 사용하는 이유를 정리해 보겠습니다.


1. component.jsx의 역할

component.jsx 파일은 해당 컴포넌트의 실제 구현을 담당하는 파일입니다. 보통 React의 function component 또는 class component를 정의하고, 필요한 JSX 및 로직을 포함합니다.

import React from 'react';

const MyComponent = () => {
  return <div>안녕하세요, 저는 MyComponent입니다!</div>;
};

export default MyComponent;

 

이처럼 component.jsx 파일에는 해당 컴포넌트의 UI와 기능이 정의됩니다.


2. index.js의 역할

같은 디렉터리에 위치한 index.js 파일은 보통 해당 컴포넌트를 exportexport 하는 역할을 합니다.

export { default } from './component';

 

이렇게 하면, 다른 곳에서 이 컴포넌트를 가져올 때 폴더 경로만 지정하면 됩니다.

 

 

예제: index.js 사용 전후 비교

(1) index.js 없이 직접 import 하는 경우

import MyComponent from '../components/MyComponent/component';

 

(2) index.js를 활용하는 경우

import MyComponent from '../components/MyComponent';

 

이처럼 index.js 파일을 두면 import 경로를 짧고 간결하게 유지할 수 있습니다.


3. index.js를 함께 사용하는 이유

폴더 경로를 간결하게 유지

  • index.js를 사용하면 컴포넌트를 가져올 때 폴더 경로를 직접 지정할 수 있어 코드가 깔끔해집니다.

파일 구조의 일관성 유지

  • 모든 컴포넌트 폴더에 index.js를 포함하면 프로젝트 구조가 일관성을 갖게 됩니다.

추후 확장 가능성 증가

  • 여러 개의 관련된 컴포넌트를 한 폴더에서 관리할 때, index.js를 이용해 한꺼번에 export 할 수도 있습니다.
export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as Sidebar } from './Sidebar';

 

이렇게 하면 한 번의 import로 여러 개의 컴포넌트를 가져올 수 있습니다.

import { Header, Footer, Sidebar } from '../components/Layout';

Summary

React에서 component.jsx와 index.js가 함께 존재하는 이유는 코드의 가독성과 유지보수성을 높이고,  import 경로를 단순화하기 위해서입니다. 

index.js를 사용하면 폴더 구조를 정리할 때 더 체계적으로 관리할 수 있고, 프로젝트의 일관성을 유지하는 데에도 큰 도움이 됩니다.

 

Spring Boot로 JPA를 사용해 엔티티 클래스를 만들다 보면, @Entity, @Table, @Id, @GeneratedValue 등의 JPA 관련 어노테이션에서 에러가 발생할 수 있습니다. 


문제 상황

다음과 같은 코드에서 @Entity, @Table, @Id, @GeneratedValue 등에 빨간 밑줄이 그어지면서 컴파일 에러가 발생하는 경우,

에러 메시지는 IDE에 따라 다르지만, STS4 기준으로 다음과 같은 오류가 발생할 수 있습니다.

이는 JPA 관련 클래스를 제대로 인식하지 못해서 발생하는 문제입니다.


원인 분석

이 문제는 JPA 라이브러리가 프로젝트에 포함되지 않아서 발생하는 경우가 많습니다.

기본적으로 Spring Boot 프로젝트에서 JPA를 사용하려면 spring-boot-starter-data-jpa 의존성을 추가해야 합니다.

Gradle 기반 프로젝트에서 build.gradle 파일에 해당 라이브러리를 추가하지 않았다면, JPA 관련 어노테이션이 인식되지 않습니다.


해결 방법

1. build.gradle에 JPA 의존성 추가

프로젝트에 있는 build.gradle을 열어 JPA 관련 라이브러리를 추가합니다.

 

그리고 Gradle 빌드를 다시 로드합니다(build.gradle 우클릭 -> Gradle -> Refresh Gradle Project).


결과

JPA 관련 어노테이션이 문제없이 import 된 것을 확인할 수 있습니다.

오늘은 react-app-rewired 설정을 통해 React 프로젝트의 빌드 파일 구조를 커스터마이징하고, 필요에 따라 react-app-rewired를 삭제한 후 빌드 파일 정리까지 하는 과정을 알아보고자 합니다.


1. react-app-rewired 설정하기

React 프로젝트에서는 기본적으로 create-react-app을 사용하면 빌드 파일의 구조가 고정되어 있습니다. 이때, react-app-rewired를 사용하면 Webpack 설정을 수정할 수 있습니다.

 

1.1 react-app-rewired 설치

먼저 프로젝트 루트 경로에서 다음 명령어를 실행하여 react-app-rewired와 customize-cra를 설치합니다.

npm install react-app-rewired customize-cra --save-dev

 

 

1.2 config-overrides.js 파일 생성

프로젝트 루트 디렉터리에 config-overrides.js 파일을 생성하고, 다음과 같이 Webpack 설정을 수정합니다.

const path = require('path');

module.exports = function override(config, env) {
  if (env === 'production') {
    // 빌드 output 경로 및 파일 이름 설정
    config.output = {
      ...config.output,
      path: path.resolve(__dirname, 'build'),
      filename: 'js/[name].[contenthash:8].js',
      publicPath: '/',
    };

    // CSS 파일의 경로 및 이름 설정
    const miniCssPlugin = config.plugins.find(
      (plugin) => plugin.constructor.name === 'MiniCssExtractPlugin'
    );

    if (miniCssPlugin) {
      miniCssPlugin.options.filename = 'css/[name].[contenthash:8].css';
    }
  }
  return config;
};

 

1.3 package.json 수정

React의 빌드, 시작 스크립트를 react-app-rewired로 변경합니다.

"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test"
}

2. 빌드 파일 정리: static 폴더 삭제하기

빌드 후 static 폴더 내부의 불필요한 파일을 삭제하고 싶다면 rimraf를 사용하여 자동으로 삭제할 수 있습니다.

 

2.1 rimraf 설치

rimraf는 Node.js 환경에서 파일과 폴더를 삭제할 수 있는 유틸리티입니다. 다음 명령어로 설치합니다.

npm install rimraf --save-dev

 

오늘은 Docker로 Oracle XE 데이터베이스를 구동하고, DBeaver를 통해 데이터베이스에 연결하는 방법을 정리해 보겠습니다.


1. Docker로 Oracle XE 컨테이너 실행

먼저 Docker로 Oracle XE 이미지를 실행합니다.

docker ps

 

Docker 컨테이너 상태를 확인했을 때 다음과 같이 출력됩니다.

CONTAINER ID   IMAGE              COMMAND                   CREATED        STATUS         PORTS                                       NAMES
fb63fcadf010   gvenzl/oracle-xe   "container-entrypoin…"   4 months ago   Up 4 minutes   0.0.0.0:1521->1521/tcp, :::1521->1521/tcp   oracle

 

이제 컨테이너에 접속합니다.

docker exec -it oracle sqlplus

 

ID와 비밀번호를 입력합니다:

  • ID: your_oracle_id
  • 비밀번호: your_oracle_password

2. Oracle XE의 SID 확인

SQL*Plus에서 다음 명령어로 SID 값을 확인합니다.

SELECT instance_name FROM v$instance;

 

출력 결과

INSTANCE_NAME
----------------
XE

 

SID 값이 XE임을 확인할 수 있습니다.


3. DBeaver 설정하기

새 데이터베이스 연결  - ORACLE 선택 - 다음 클릭

 

DBeaver에서 다음과 같이 설정합니다

 

  • Host: localhost
  • Port: 1521
  • Database: XE (SID 값)
  • Service Name 또는 SID 옵션 선택
  • Username: your_oracle_id
  • Password: your_oracle_password

4. DBeaver 연결 테스트

이제 "Test Connection" 버튼을 눌러 연결 테스트를 합니다. 연결이 성공하면 설정을 저장하고, 데이터베이스에 접속할 수 있습니다.

 

AWS EC2 또는 다른 서버에 SSH로 접속할 때, 비활성 상태로 인해 client_loop: send disconnect: Broken pipe 오류가 발생할 수 있습니다.

이런 오류는 유휴 상태(사용자가 명령을 입력하거나 데이터를 송수신하지 않는 상태)에서 연결이 자동으로 끊어질 때 발생합니다.

이러한 문제를 방지하기 위해서는 SSH 설정 파일에 주기적으로 서버에 신호를 보내도록 설정하여 연결을 유지할 수 있습니다.


설정 방법

1. SSH 설정 파일 열기(vi 또는 nano로 파일 열기)

vi ~/.ssh/config

 

 

2. 설정 추가 - 설정 파일이 없을 경우 새로 만들어서 추가하세요.

Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
  • Host * : 모든 호스트에 대해 설정을 적용
  • ServerAliveInterval 60 : 60초마다 서버에 신호를 보내 연결을 유지
  • ServerAliveCounMax 3 : 서버가 응답하지 않으면 최대 3번까지 신호를 보내고 그 이후에도 응답이 없으면 연결 종료
  • TCPKeepAlive yes : TCP 연결을 유지하도록 설정하고 네트워크 라우터가 유휴 연결을 강제로 종료하지 않도록 함

3. 파일 저장 및 종료

  • ESC 키를 누른 후 :wq 명령어로 저장하고 편집기를 종료합니다.

이 설정을 적용하면 SSH 연결이 더 안정적으로 유지되며, ASW EC2와 같은 원격 서버에서 Broken Pipe 오류가 발생할 가능성이 줄어듭니다.

 

 

 

 

Spring Boot를 백엔드로 사용하면서 React를 프론트엔드로 통합하는 방법을 정리합니다.

JSP 기반의 기존 프로젝트에서 React를 일부 도입하고 싶을 때 적용할 수 있는 방식입니다.


1. 프로젝트 구조

/backend
  ├── src/main/java/com/cardfolio/springboot
  │       ├── CardFolioApplication.java
  │       ├── controller
  │       │     ├── WebController.java
  │       └── ...
  ├── src/main/resources/static (React 빌드 파일 배포 위치)
  ├── src/main/resources/templates (JSP 파일 위치)
  ├── src/main/resources/application.properties
  
/frontend
  ├── public
  ├── src
  ├── package.json
  ├── index.html
  └── ...

2. Spring Boot 설정

React 정적 리소스를 서빙할 수 있도록 application.properties를 다음과 같이 수정합니다.

# JSP 뷰 설정
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# React 정적 리소스 제공 설정
spring.resources.static-locations=classpath:/static/
spring.web.resources.add-mappings=true

 

기존 JSP를 유지하면서 React의 정적 리소스를 서빙할 수 있도록 spring.resources.static-locations 설정을 추가합니다.


3. React 프로젝트 빌드 및 배포

React 프로젝트를 빌드한 후, Spring Boot의 static 폴더에 배포합니다.

  1. React 프로젝트 빌드 실행
cd frontend
npm run build

 

   2. 빌드된 파일을 백엔드 프로젝트로 복사

cp -r build/* ../backend/src/main/resources/static/

4. React 라우팅 문제 해결(Spring Boot 설정)

React의 SPA(Single Page Application) 특성상, 직접 URL로 접근하면 404 오류가 발생할 수 있습니다.

이를 방지하기 위해 Spring Boot에서 모든 경로를 index.html로 포워딩하도록 컨트롤러를 추가합니다.

 

WebController.java

package com.cardfolio.springboot;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class WebController {
    @GetMapping({"/", "/chart", "/card", "/company"})
    public String forwardReact() {
        return "forward:/index.html";
    }
}

 

사용자가 /chart, /card, /company 등의 경로로 접근할 때 React의 index.html이 제공되도록 설정합니다.


5. React 라우터 설정 (프론트엔드 설정)

React의 BrowserRouter는 기본적으로 HTML5의 history API를 사용하므로, Spring Boot 서버에서 경로를 처리할 수 있도록 조정해야 합니다.

 

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom'; 

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>
);

 

App.js

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Layout from './Component/Layout';
import HomePage from './Pages/HomePage';
import ChartPage from './Pages/ChartPage/ChartPage';
import CardPage from './Pages/CardPage/CardPage';
import CompanyPage from './Pages/CompanyPage/CompanyPage';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout><HomePage /></Layout>} />
      <Route path="/chart" element={<Layout><ChartPage /></Layout>} />
      <Route path="/card" element={<Layout><CardPage /></Layout>} />
      <Route path="/company" element={<Layout><CompanyPage /></Layout>} />
    </Routes>
  );
}

export default App;

+ Recent posts