꿈꾸는 개발자 https://studioys.me #개발 #일상생활 #생각 Mon, 20 Jan 2025 00:19:59 +0000 ko-KR hourly 1 https://wordpress.org/?v=6.8 https://studioys.me/wp-content/webpc-passthru.php?src=https://studioys.me/wp-content/uploads/2024/09/cropped-그림1-32x32.png&nocache=1 꿈꾸는 개발자 https://studioys.me 32 32 [Flutter] Flutter 프로젝트 구성 및 가상 디바이스 https://studioys.me/how-to-set-up-a-flutter-project-and-a-virtual-device/ https://studioys.me/how-to-set-up-a-flutter-project-and-a-virtual-device/#respond Tue, 14 Jan 2025 04:03:52 +0000 https://studioys.me/?p=1245 더 보기[Flutter] Flutter 프로젝트 구성 및 가상 디바이스]]> 플러터(Flutter)는 크로스 플랫폼으로 하나의 코드 베이스로 다양한 앱을 만들 수 있습니다. 이번 포스팅에서는 Android 및 iOS 앱을 대상으로 Flutter 프로젝트 구성하고 가상 디바이스를 통해서 앱을 실행시켜보겠습니다.

사전 준비 사항

맥북에서 인텔리제이(IntellIJ) 기반으로 개발하기 때문에 개발 환경 구성이 되어 있어야 합니다.

만약 환경 구성이 안되어 있다면 다음 포스트를 참고하여 개발 환경 구성을 해주세요.

Flutter 프로젝트 구성

프로젝트 생성

Flutter 프로젝트를 생성하고, Platforms에서 AndroidiOS를 선택합니다.

Dart 패키키 이름은 소문자와 _ 만 허용하기 때문에 소문자와 _ 를 사용하여 프로젝트 이름을 생성해주세요.

Android SDK 설정

[설정] 메뉴로 들어가서 [언어 및 프레임워크] > [Android SDK 업데이트 관리자] 항목을 선택합니다. 아래와 같이 Android SDK 위치가 지정되어있지 않다면 Android SDK 위치를 지정합니다.

Android 패싯 추가

[파일] > [프로젝트 구조] 메뉴로 들어가서 [패싯] 항목을 선택 후 Andriod를 추가합니다.

가상 디바이스

정상적으로 개발 환경이 구성되었고, 프로젝트에 SDK 및 패싯을 정상적으로 추가했으면 아래와 같이 iOS Simulator와 Android Emulator를 확인할 수 있습니다.

인텔리제이 가상 디바이스 선택
인텔리제이 가상 디바이스 선택

iOS Simulator 또는 Android Emulator을 선택하면 가상 디바이스가 실행되고, 해당 디바이스를 선택하고 디버그 버튼을 누르면 가상 디바이스에 연결하여 디버깅을 할 수 있습니다.

인텔리제이(IntelliJ) iOS Simulator 실행
인텔리제이(IntelliJ) iOS Simulator 실행화면

마치며

이제 프로젝트를 구성하고, 가상 디바이스를 실행해보았습니다. 본격적으로 앱을 만들어보겠습니다.

]]>
https://studioys.me/how-to-set-up-a-flutter-project-and-a-virtual-device/feed/ 0
[Flutter] Flutter 개발 환경 설정 ( 맥북, macOS ) https://studioys.me/how-to-set-up-flutter-development-env-on-macbook/ https://studioys.me/how-to-set-up-flutter-development-env-on-macbook/#respond Mon, 13 Jan 2025 13:33:13 +0000 https://studioys.me/?p=1219 더 보기[Flutter] Flutter 개발 환경 설정 ( 맥북, macOS )]]> 플러터(Flutter)는 구글에서 출시한 크로스 플랫폼으로 Dart 언어를 기반으로 하나의 코드 베이스로 Linux, Windows, Android, macOS, iOS 및 웹 브라우저에서 모두 동작하는 앱을 만들 수 있습니다. 주로 Android 및 iOS 앱을 만들기 위한 용도로 많이 사용하고 있습니다.

이번 포스팅에서는 맥북에서 Android 및 iOS 앱을 만들기 위한 Flutter 개발 환경 및 인텔리제이 설정에 대해서 알아보겠습니다.

시스템 요구 사항

Flutter 개발을 위한 최소 시스템 요구사항은 다음과 같습니다.

  • 운영 체제: MacOS 11 (Big Sur) 이상 ( 최신 버전 권장 )
  • CPU: 최소 4 Core ( 8Core 권장 )
  • RAM: 최소 8GB ( 16GB 권장 )
  • 디스크 공간: 최소 44GB의 여유 공간 ( 70GB 이상 권장 )
  • 해상도: 최소 WXGA(1366×768) 이상의 화면 해상도 ( 1928×1080 권장 )

Flutter SDK 설치

Flutter SDK 다운로드 및 설치

Flutter를 설치하려면 다운로드 페이지에서 Flutter SDK 파일을 다운로드하고 원하는 폴더에 압축을 해제합니다.

  1. 최신 버전 또는 프로젝트에서 사용하는 버전을 다운로드 합니다.
  2. 원하는 위치에 폴더를 생성 후 해당 폴더에 압축을 풀면 설치가 완료됩니다.
    공식 가이드에서는 ~/development 폴더를 권장합니다.

Flutter 경로 추가

터미널에서 Flutter 명령을 실행하기 위해서 ~/.zshrc 파일 맨 뒤에 PATH 환경 변수에 Fultter 경로를 추가하고, source ~/.zshrc 명령으로 새로고침 합니다.

export PATH=$HOME/development/flutter/bin:$PATH

정상적으로 PATH 경로가 등록 되었는지 확인해보겠습니다.

flutter --version
실행 결과
Flutter 3.19.6 • channel stable • https://github.com/flutter/flutter.git

Flutter 개발 환경 구성 – Android

안드로이드(Android) 앱을 개발하기 위해서는 Android Studio를 먼저 설치해야 합니다.

Android 앱 개발을 위해서 다음 포스트를 참고하여 Android Studio 설치 및 설정을 해주세요.

Flutter 개발 환경 구성 – iOS

iOS 앱을 개발하기 위해서는 XcodeCocoaPods를 설치해야 합니다.

iOS 앱 개발을 위해서 다음 포스트를 참고하여 Xcode 및 CocoaPods 설치 및 설정을 해주세요.

인텔리제이(IntelliJ) 설정

Flutter 개발을 위한 인텔리제이(IntelliJ) 설정을 해보겠습니다. ( IntelliJ IDEA 2024.3.3.1 버전 기준 )

플러그인 설치

[설정] > [플러그인] 메뉴에서 Dart, Flutter를 설치합니다.

인텔리제이(IntelliJ) 플러그인
IntelliJ 플러그인

플러그인 설치 완료 후 IntelliJ를 재시작합니다.

신규 프로젝트 생성

IntelliJ에서 신규 프로젝트를 생성하면 아래 그림과 같이 Flutter이 추가된 것을 확인할 수 있습니다. Flutter SDK Path에 Flutter 경로를 설정하고, 프로젝트를 생성하면 됩니다.

인텔리제이(IntelliJ) Flutter 프로젝트
IntelliJ에서 Flutter 프로젝트 생성 화면

기존 프로젝트 추가

기존 Flutter 프로젝트가 있는 경우 [설정] > [언어 및 프레임워크] > [Flutter] 메뉴에 가면 Flutter SDK Path를 설정할 수 있습니다.

인텔리제이(IntelliJ) Flutter SDK 경로 설정
IntelliJ Flutter SDK 경로 설정

마치며

맥북에서 Flutter 개발 환경 설정을 완료했습니다. 이제 크로스 플랫폼으로 Android 앱 개발 및 iOS 앱 개발을 시작해보겠습니다.

]]>
https://studioys.me/how-to-set-up-flutter-development-env-on-macbook/feed/ 0
[Flutter] iOS 앱 개발 환경 설정 ( Xcode 및 CocoaPods ) https://studioys.me/how-to-set-up-an-ios-app-development-env-for-flutter/ https://studioys.me/how-to-set-up-an-ios-app-development-env-for-flutter/#respond Sun, 12 Jan 2025 12:37:20 +0000 https://studioys.me/?p=1222 더 보기[Flutter] iOS 앱 개발 환경 설정 ( Xcode 및 CocoaPods )]]> 이 글은 Flutter 개발 환경 설정 가이드의 하위 포스트로, Flutter로 iOS 앱을 개발하기 위한 환경을 설정하는 방법을 설명합니다.

(2025년 01월 12일 기준) Flutter로 iOS 앱을 개발하려면 Xcode 16 버전 및 CocoaPods 1.16 버전이 필요합니다. 이번 포스팅에서는 맥북(macOS)에서 iOS 앱 개발 환경 설정 방법에 대해서 알아보겠습니다.

iOS 앱 개발 환경 설정

Xcode 설치 및 환경 설정

iOS 앱 개발을 위해 Xcode를 설치해야 합니다.

Xcode 설치

  1. App Store를 열고, Xcode를 검색하여 설치합니다.
  2. 설치가 완료되면 Xcode를 실행하여 필요한 구성 요소를 다운로드합니다.

Xcode 라이선스 동의

Xcode를 처음 사용할 때는 사용 라이선스에 동의해야 합니다. Xcode 실행 후 라이선스 동의하거나 터미널 명령어를 통해 라이선스 동의할 수 있습니다.

터미널을 열고 아래 명령을 입력하고 라이선스 내용을 확인한 뒤, agree를 입력하여 라이선스에 동의합니다.

sudo xcodebuild -license

명령줄 도구(Command Line) 설정

설치된 Xcode 버전을 명령줄 도구(Command Line)로 설정하기 위해 터미널에서 아래 명령어를 입력합니다.

sudo sh -c 'xcode-select -s /Applications/Xcode.app/Contents/Developer && xcodebuild -runFirstLaunch'

루비(Ruby) 버전 확인 및 업데이트

CocoaPods를 설치하려면 최신 버전의 Ruby가 필요합니다.

루비 버전 확인

터미널에서 아래 명령어로 현재 설치된 Ruby 버전을 확인합니다.

ruby -v
실행 결과
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin24]

(2025년 01월 12일 기준) CocoaPods 1.16버전은 Ruby 3.1.0 버전이 필요합니다.Homebrewrbenv를 사용하여 Ruby 3.1.0을 설치합니다.

Homebrew가 설치되어 있지 않다면 Homebrew 설치 가이드를 참고하세요.

1. rbenv 설치

brew install rbenv

2. ruby 3.1.0 설치

rbenv install 3.1.0
rbenv global 3.1.0

3. ruby 버전 확인

ruby -v

환경 변수 설정

루비 버전이 반영되지 않았다면 환경 변수를 설정합니다.
~/.zshrc 파일에 아래 내용을 추가하고 source ~/.zshrc 명령을 입력하여 적용합니다.

eval "$(rbenv init -)"

CocoaPods 설치

CocoaPods는 iOS 프로젝트의 의존성을 관리하는 도구입니다.

CocoaPods 설치

터미널에서 아래 명령을 입력하여 CocoaPods를 설치합니다. 관리자 패스워드를 입력하고, 시간이 조금 지나면 설치가 시작됩니다.

sudo gem install cocoapods

환경 변수 설정

CocoaPods 설치 후, 환경 변수를 추가합니다.
~/.zshrc 파일에 아래 내용을 추가하고 source ~/.zshrc 명령을 입력하여 적용합니다.

export PATH=$HOME/.gem/bin:$PATH

iOS 시뮬레이터 설정

가상 디바이스 다운로드

아래 명령어로 필요한 iOS 플랫폼 파일을 다운로드합니다.

xcodebuild -downloadPlatform iOS

시뮬레이터 실행

터미널에서 아래 명령어로 iOS 시뮬레이터를 실행합니다.

open -a Simulator

시뮬레이터가 정상적으로 실행되면 설치가 완료된 것입니다.

개발 환경 체크

Flutter Doctor 실행

터미널에서 아래 명령을 실행하여 환경 구성이 정상적으로 완료되었는지 확인합니다.

flutter doctor
실행 결과
Running flutter doctor...
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.0, on macOS 15.2 24C101 darwin-arm64, locale ko-KR)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[!] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] IntelliJ IDEA Ultimate Edition (version 2024.3.1.1)
[✓] Connected device (1 available)
[✓] Network resources

Xcode - develop for iOS and macOS (Xcode 16.2) 항목이 정상적으로 체크되었다면 iOS 앱 개발 환경 설정이 완료된 것입니다.

마치며

Flutter에서 iOS 앱 개발을 시작하기 위해 필요한 환경 설정을 마쳤습니다. 이제 Flutter로 iOS 앱 개발을 시작해보세요.!!

참고 자료

]]>
https://studioys.me/how-to-set-up-an-ios-app-development-env-for-flutter/feed/ 0
[Flutter] Android 앱 개발 환경 설정 ( Android Studio ) https://studioys.me/how-to-set-up-android-app-development-env-for-flutter/ https://studioys.me/how-to-set-up-android-app-development-env-for-flutter/#respond Fri, 10 Jan 2025 12:32:50 +0000 https://studioys.me/?p=1199 더 보기[Flutter] Android 앱 개발 환경 설정 ( Android Studio )]]>

이 글은 Flutter 개발 환경 설정 포스트의 하위 포스트입니다.

(2025년 01월 12일 기준) Flutter로 안드로이드(Android) 앱 개발을 하기 위해서는 안드로이드 스튜디오(Android Studio)가 필요합니다. 이번 포스팅에서는 Android 앱 개발 환경 설정 방법에 대해서 알아보겠습니다.

Android 앱 개발 환경 설정

안드로이드 스튜디오(Android Studio)

안드로이드 스튜디오는 Google에서 제공하는 공식 통합 개발 환경 도구로 안드로이드 앱 개발에 최적화되어 있습니다.

시스템 요구 사항

안드로이드 스튜디오 최소 시스템 요구 사항은 다음과 같습니다.

  • 운영 체제: MacOS 10.14 – Mojave 이상 ( 최신 버전 권장 )
  • CPU: Apple M1 칩 또는 2세대 Intel Core 이상 ( 하이퍼바이저 프레임워크 지원 필요 )
  • RAM: 최소 8GB (16GB 권장)
  • 디스크 공간: 최소 8GB의 여유 공간 ( SSD 16GB 이상 권장 )
  • 해상도: 1280×800 이상의 화면 해상도 ( 1928×1080 권장 )

안드로이드 스튜디오 다운로드

Android Developers 다운로드 페이지에서 Mac 버전을 다운로드 합니다.

안드로이드 스튜디오 설치 파일 다운로드

안드로이드 스튜디오 설치

다운로드한 .dmg 파일을 열고 Android Studio 아이콘을 Applications 폴더로 드래그하면 안드로이드 스튜디오를 설치할 수 있습니다.

안드로이드 스튜디오 설치

Android Studio를 실행합니다. 최초 설치 시 Setup Wizard가 실행되는데, Standard로 설치하면 됩니다.

안드로이드 스튜디오 설정

Android SDK 설정

안드로이드 스튜디오를 실행하고, 메뉴에서 [Andorid Studio] > [Settings…] 메뉴를 클릭하면 아래와 같이 설정 화면이 팝업되는데, [Language & Framework] > [Android SDK] 항목으로 들어가서 SDK Platforms 및 SDK Tools에서 아래 항목을 체크하여 설치합니다.

  • Android SDK Platform, API 35.0.1
  • Android SDK Build-Tools
  • Android SDK Command-line Tools
  • Android Emulator
  • Android SDK Platform-Tools
안드로이드 스튜디오 설정

Android License 동의

터미널을 열고 다음과 같이 입력합니다.

flutter doctor --android-licenses

라이선스에 동의 여부를 묻는 프롬프트가 나오면 y를 눌러 모든 항목에 대해서 동의 합니다.

개발 환경 체크

flutter doctor로 정상적으로 구성되었는지 확인해보겠습니다. 터미널을 열고 아래 명령을 입력해주세요.

Andriod toolchainAndroid Studio 항목이 정상으로 체크되어 있으면 환경 구성이 정상적으로 설정된것입니다.

flutter doctor
실행 결과
Running flutter doctor...
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.0, on macOS 15.2 24C101 darwin-arm64, locale ko-KR)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[!] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] IntelliJ IDEA Ultimate Edition (version 2024.3.1.1)
[✓] Connected device (1 available)
[✓] Network resources

가상 디바이스 설정 ( Android emulator )

Android Studio를 실행합니다. 초기 화면에서는 [More Actions]에서 [Virtual Device Manager] 메뉴를 클릭하고, 만약 프로젝트가 열려져 있다면, [Tool] > [Device Manager] 메뉴를 클릭합니다.

Virtual Device Manager

보통 위와 같이 가상 장치가 등록되어 있는데, 다른 장치가 필요하거나 장치가 없을 경우에는 + 버튼을 눌러 장치를 추가할 수 있습니다.

마치며

Flutter에서 안드로이드 앱 개발을 위한 안드로이드 스튜디오 설치 및 환경 설정을 해보았습니다.

]]>
https://studioys.me/how-to-set-up-android-app-development-env-for-flutter/feed/ 0
var let const 차이점 https://studioys.me/var-let-const-difference/ https://studioys.me/var-let-const-difference/#respond Sat, 04 Jan 2025 13:46:13 +0000 https://studioys.me/?p=668 더 보기var let const 차이점]]> 자바스크립트에서 변수를 선언할 때 var let const 키워드를 사용합니다. 이번 포스팅에서는 var let const 차이점 및 사용 방법에 대해서 알아보도록 하겠습니다.

변수 선언

변수를 사용하기 위해서는 변수 선언이 필요합니다. 자바스크립트에서는 변수를 선언할 때 var let const 키워드를 사용합니다.

var 키워드는 ES6(ES2015) 이전부터 사용하던 키워드로 블록 레벨 스코프(Block-Level Scope)를 지원하지 못하고, 함수 레벨 스코프(Function-Level Scope)를 지원하여 개발자가 의도하지 않는 오류가 발생할 가능성이 높기 때문에, ES6에서는 보다 엄격한 언어적 특성을 추가하여 var를 대체하는 용도로 let과 const 키워드를 도입했습니다.

let, const 가 도입되었다고 해서 var가 완전히 폐기된 것은 아닙니다. ES6은 하위 호환성을 기본적으로 보장하고 있어서, ES6 이전에 작성한 스크립트는 여전히 유효하고 var도 여전히 유효합니다. 다만, var를 대체하는 용도로 let, const가 도입되었기 때문에 var 보다는 letconst 키워드 사용을 권장하고 있습니다.

var let const 차이점

var let const 차이점은 다음과 같습니다.

변수 선언 방식 및 재할당 가능 여부

const상수를 선언하는 키워드로 선언과 동시에 초기화를 해야 합니다. 그리고 해당 변수에 다른 값을 재할당할 수 없습니다.

let, var는 선언 및 초기화를 자유롭게 할 수 있고, 선언 및 초기화 이후 재할당이 가능합니다.

const x = 1;   // 선언과 동시에 초기화
x = 2;         // 재할당 할 경우 다음과 같이 오류 발생         
// Uncaught TypeError: Assignment to constant variable.

const y;       // 선언만 한 경우 다음과 같이 오류 발생
// Uncaught SyntaxError: Missing initializer in const declaration

var a = 1;     // 선언과 동시에 초기화
a = 2;         // 재할당 가능

let b = 1;     // 선언과 동시에 초기화
b = 2;         // 재할당 가능

var c;         // 선언만 가능
let d;         // 선언만 가능

중복 선언 가능 여부

var중복 선언이 가능하여 기존에 선언한 변수를 코드 뒤쪽에서 다시 중복 선언하고 사용하여 예상치 못한 곳에서 오류가 발생할 수 있습니다.

var name = "StudioYS";
console.log(name);    // StudioYS

var name = "Narine";
console.log(name);    // Narine

let age = 27;
console.log(age);     // 27

let age = 31;         // 중복 선언 오류 발생
// Uncaught SyntaxError: Identifier 'age' has already been declared

변수 유효 범위(scope)

변수 유효 범위(scope)는 변수를 참조(사용)할 수 있는 범위를 말합니다. 대부분의 프로그래밍 언어는 코드 블럭 { … } 내부에서만 유요한 블록 레벨 스코프를 많이 사용하고 있으나, var의 경우는 코드 블럭과 상관없이 함수 내부에서 모두 사용 가능한 함수 레벨 스코프를 사용합니다. 이로 인하여 예상치 못한 곳에서 오류가 발생할 수 있습니다.

function Scope1() {
  var x = 0;
  if ( x === 0 ) {
    var y = 1;
  }
  
  console.log(x);
  console.log(y);  // if 코드 블럭을 벗어났는데 사용 가능
}

function Scope2() {
  let x = 0;
  if ( x === 0 ) {
    let y = 1;     // if 안에서면 유효
  }
  
  console.log(x);
  console.log(y);  // if 코드 블럭을 벗어났는데 사용 가능
  // Uncaught ReferenceError: y is not defined
}

Scope1();
Scope2();

호이스팅(Hoisting)

호이스팅은 변수 선언문이나 함수 선언문 등을 해당 스코프의 맨 앞으로 옮긴 것 처럼 동작하는 특성을 말합니다.이러한 특성 때문에 변수나 함수를 코드 뒤 쪽에 선언해도 문제 없이 사용할 수 있습니다.

var는 호이스팅이 발생하면 자동으로 변수 초기 값을 undefined로 선언해 실제 값을 할당하기 전 까지 undefined로 참조합니다.

console.log(a);    // undefined 출력
var a = 10;
console.log(a);    // 10 출력

위 코드는 호이스팅이 발생하면 다음과 같이 표현할 수 있습니다.

var a = undefined; // var 변수 호이스팅
console.log(a);    // undefined 출력
a = 10;
console.log(a);    // 10 출력

하지만 let은 이와 다르게 동작합니다. 변수를 선언하기 전에 사용하면 정의되어있지 않다고 오류가 발생합니다.

console.log(a);    // 오류 발생 - ReferenceError: a is not defined 
let a = 10;
console.log(a);    // 10 출력

위 코드는 호이스팅이 발생하면 다음과 같이 표현할 수 있습니다.

let a;            // 선언만 하고 정의하지 않음 - 사용 시 오류 발생
console.log(a);   // 오류 발생 - ReferenceError: a is not defined
var a = 10;
console.log(a);   // 10 출력

결론

자바스크립트에서 var let const의 특징은 다음과 같이 요약할 수 있습니다.

var let const 차이점 정리 표

그리고 자바스크립트에서 변수를 사용하는 데 있어 다음과 같이 사용하도록 권고합니다.

  1. 재할당이 필요없는 경우 const 키워드를 사용하자
  2. let const 키워드를 우선으로 사용하자
  3. var 키워드는 절대로 사용하지 말자

참고 자료

]]>
https://studioys.me/var-let-const-difference/feed/ 0
Rocky Linux에 nginx 설치 및 Let’s Encrypt 설정 https://studioys.me/how-to-install-and-setup-nginx-and-lets-encryt/ https://studioys.me/how-to-install-and-setup-nginx-and-lets-encryt/#respond Tue, 31 Dec 2024 13:33:36 +0000 https://studioys.me/?p=956 더 보기Rocky Linux에 nginx 설치 및 Let’s Encrypt 설정]]> 클라우드 환경에서 Rocky Linux 서버 인스턴스 생성 후, nginx 설치 및 Let’s Encrypt 설정하는 방법에 대해서 알아보도록 하겠습니다.

아래 그림과 같이 하나의 서버에서 여러 웹 서버를 운영하면서 nginx를 사용해서 호스트에 따라 적절한 포트로 라우팅하도록 설정하는 방법에 대해서 알아보겠습니다.

그림. 서버 구성도

nginx 설치

시스템 업데이트

서버 접속 후 Rocky Linux 최소 보안 업데이트를 진행합니다.

sudo dnf -y upgrade-minimal

nginx 저장소 추가

Rocky Linux의 기본 패키지 저장소에도 nginx가 있지만, 최신 버전을 설치하려면 EPEL(Extra Packages for Enterprise Linux) 저장소를 사용하는 것이 좋습니다.

sudo dnf install epel-release -y

nginx 설치

EPEL 저장소를 추가한 후, 다음 명령어를 사용해 nginx를 설치합니다.

sudo dnf install nginx -y

nginx 서비스 시작 및 부팅 시 자동 시작 설정

nginx를 설치한 후에는 서비스를 시작하고, 부팅 시 자동으로 시작되도록 설정합니다.

# nginx 서비스 시작
sudo systemctl start nginx

# 부팅 시 자동 시작 설정
sudo systemctl enable nginx

방화벽 설정

nginx가 외부에서 접근할 수 있도록 방화벽 설정을 추가해야 합니다. HTTP와 HTTPS 트래픽을 허용합니다.

# HTTP(80번 포트) 허용
sudo firewall-cmd --permanent --add-service=http

# HTTPS(443번 포트) 허용
sudo firewall-cmd --permanent --add-service=https

# 방화벽 재로드
sudo firewall-cmd --reload

설치 확인

nginx가 제대로 설치 되었는지 확인하려면 웹 브라우저에서 Rocky Linux 서버의 IP 주소로 접속해 봅니다. 아래 명령어로 서버 IP 주소를 확인할 수 있습니다.

hostname -I

웹 브라우저에서 http://서버_IP주소로 접속했을 때 nginx 테스트 페이지가 보이면 정상적으로 nignx가 설치된것입니다.

nginx 테스트 페이지

Let’s Encrypt 설정

certbot 설치

certbot을 설치하기 위해서는 EPEL(Extra Packages for Enterprise Linux) 저장소를 사용하도록 해야 합니다. nginx를 설치할 때 이미 했기 때문에 생략해도 됩니다.

# enable EPEL
sudo dnf install epel-release -y 

# certbot 설치
sudo dnf install certbot -y 

SSL 인증서 획득

Let’s Encrypt를 획득하기 위해서는 여러 방법이 있는데, 가장 무난한 standalone 방식으로 인증서를 획득하겠습니다. 인증을 받기 위해 nginx 서버를 잠시 정지합니다.

certbot 명령어에서 -d 옵션을 여러 개 입력하여 여러 도메인을 한번에 인증 받을 수 있는데, 개인적으로 하나씩 받는걸 추천합니다. 또 와일드 카드 인증서는 DNS 인증을 받아야 하기 때문에 자동 연장이 안되서 standalone 방식으로 인증서 만드는 것을 추천합니다.

# nginx 정지
sudo systemctl stop nginx

# SSL 인증서 획득
sudo certbot certonly --standalone -d example.com -d host.example.com -d ... -d ...

/etc/letencrypt/live 폴더에 가면 도메인 이름으로 폴더가 생성되고, 그 폴더 하위에 인증서가 생성된 것을 확인할 수 있습니다.

nginx 설정

이제 nginx 설정을 해보겠습니다. nginx 설정 폴더로 이동 후 sites-enabled 폴더를 생성합니다. sites-enabled는 도메인 별로 설정을 관리하기 위해서 생성하는 폴더입니다.

# nginx 폴더로 이동
cd /etc/nginx

# sites-enabled 폴더 생성 후 이동
mkdir sites-enabled && cd sites-enabled

# 도메인 설정 파일 생성
vi example.com.conf

도메인 설정 파일에 아래와 같이 입력 후 내용을 작성합니다. 만약 도메인이 여러 개 있다면 server { … } 부분을 여러 개 추가하면 됩니다.

# api.example.com -> http://localhost:9000 으로 라우팅
server {
  listen 443 ssl http2;
  # URL : api.example.com 
  server_name api.example.com;
  # certbot으로 생성한 인증서 위치
  ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/api.example/privkey.pem;
  ssl_session_cache shared:SSL:1m;
  ssl_session_timeout  10m;

  location / {
    # 서버 주소 입력
    proxy_pass  http://localhost:9000;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

기존 nginx 설정에 위 설정을 추가합니다.

# nginx 폴더로 이동
cd /etc/nginx

# conf 수정
vi example.com.conf
nginx conf 파일

위 그림과 같이 include /etc/nginx/sites-enabled/*.conf 를 추가합니다.

인증서 자동 갱신

Let’s Encrypt는 인증서 유효기간이 90일 이며, 만료 30일 이내에 갱신이 가능합니다. 따라서 매일 월요일 오전 3시에 자동으로 갱신할 수 있도록 crontab에 등록해보겠습니다.

우선 갱신이 정상적으로 되는 환경인지 테스트 해보겠습니다. ( nginx는 중지 상태여야 합니다. )

# 갱신 모의 테스트
sudo certbot renew --dry-run

갱신하는데 문제가 없다면, crontab에 등록하겠습니다.

crontab 설정 파일을 열고 아래 내용을 입력합니다.

# cronetab 설정 열기
crontab -e

# 월요일 오전 3시에 갱신
0 3 * * 1 /usr/bin/certbot renew --quiet --pre-hook "/usr/sbin/service nginx stop" --post-hook "/usr/sbin/service nginx start"

crontab 서비스를 재시작합니다.

systemctl restart crond.service

적용 확인

이제 nginx를 시작하여 정상적으로 인증서가 반영되어 HTTPS 처리가 되는지 확인하면 됩니다.

sudo systemctl start nginx

참고 자료 및 관련 사이트

]]>
https://studioys.me/how-to-install-and-setup-nginx-and-lets-encryt/feed/ 0
클라우드와 코로케이션(서버호스팅): 초기 스타트업 비즈니스에서 당신의 선택은? https://studioys.me/cloud-and-colocation-server-hosting/ https://studioys.me/cloud-and-colocation-server-hosting/#respond Sun, 29 Dec 2024 13:20:25 +0000 https://studioys.me/?p=1015 더 보기클라우드와 코로케이션(서버호스팅): 초기 스타트업 비즈니스에서 당신의 선택은?]]> 웹 서비스를 위해 서버를 구성하는 데 있어 다양한 방법이 존재 합니다. 그 중에서 클라우드와 코로케이션(서버호스팅)은 서버를 구성하는 데 가장 많이 선택하는 옵션으로 초기 스타트업에서 어느 것이 더 효과적인지 고민하는 경우가 많이 존재합니다.

어느 정도 규모가 되는 기업은 자체적으로 서버를 운영할 수 있는 조직과 역량을 갖추고 있어 워크로드와 보안 요구사항에 따라 클라우드와 코로케이션 중 적합한 방식을 유연하게 선택할 수 있습니다. 하지만 초기 자본이 부족한 스타트업은 비용적인 측면이 초기 생존과 직결되기 때문에 비용적인 측면에서 고민을 하는 것이 사실입니다.

이 글에서는 초기 스타트업의 특성을 반영하여 어떤 선택이 더 적합한지 알아보도록 하겠습니다.

클라우드란?

클라우드는 인터넷을 통해 컴퓨팅 자원(서버, 스토리지, 네트워크 등)을 제공하는 서비스입니다. 대표적인 클라우드 서비스 제공업체로는 AWS, Microsoft Azure, Google Cloud, Naver Cloud 등이 있습니다.

클라우드의 장점

  • 유연성: 필요에 따라 자원을 확장하거나 축소할 수 있습니다.
  • 비용 효율성: 초기 투자 비용이 없으며, 사용한 만큼만 요금을 지불합니다.
  • 신속한 배포: 인프라를 빠르게 설정하고 서비스를 시작할 수 있습니다.
  • 관리 간소화: 하드웨어 유지보수가 필요 없으며, 서비스 제공자가 이를 책임집니다.

클라우드의 단점

  • 예상치 못한 비용 증가: 사용량이 증가하면 비용도 예상보다 크게 늘어날 수 있습니다.
  • 종속성: 특정 클라우드 제공업체에 의존하게 될 가능성이 있습니다.
  • 보안 우려: 데이터가 제3자 서버에 저장되므로 보안 문제를 신중히 검토해야 합니다.

코로케이션이란?

코로케이션은 기업이 자체 서버를 구매하여 데이터센터의 공간을 임대하고, 전력, 냉각, 네트워크와 같은 인프라 서비스를 제공받는 방식입니다.

코로케이션의 장점

  • 높은 제어권: 하드웨어 및 소프트웨어를 완전히 제어할 수 있습니다.
  • 보안 강화: 데이터를 물리적으로 보호할 수 있는 독립적인 환경을 제공합니다.
  • 예측 가능한 비용: 장기적인 관점에서 클라우드보다 비용이 저렴할 수 있습니다.
  • 고성능: 특정 워크로드에 맞게 하드웨어를 최적화할 수 있습니다.

코로케이션의 단점

  • 초기 투자 비용: 서버 장비 구매 및 설치 비용이 높습니다.
  • 복잡한 관리: 하드웨어 유지보수 및 업그레이드가 필요합니다.
  • 확장성의 한계: 급격한 확장 요구를 처리하기 어려울 수 있습니다.

클라우드와 코로케이션의 주요 차이점

특징클라우드코로케이션
비용 구조사용량 기반 과금초기 투자 비용 및 월 운영비
확장성무제한 확장 가능제한적, 하드웨어 추가 필요
제어권제한적, 서비스 제공 업체에 의존S/W 및 H/W 모두 제어 가능
보안공유 인프라 기반, 보안 우려 있음물리적 제어 가능, 보안 강화
유지보수서비스 제공업체가 관리자체 유지보수 필요
가용성글로벌 네트워크 지원데이터센터의 물리적 위치에 의존

초기 스타트업에서 고려사항

클라우드와 코로케이션 선택에서 가장 중요한 요소는 서버 스펙 및 인스턴스 개수 입니다. 초기 사용자가 많지 않다고 가정하고, 소규모로 운영 가능한 최소 서버 스펙을 vCPU 1코어 및 2GB 램으로 가정해보겠습니다.

서버 비용

클라우드와 코로케이션을 모두 제공하는 가비아를 기준으로 비교해보겠습니다.

가비아 클라우드 비용
가비아 클라우드 비용 ( 1vCore / 2GB )
가비아 클라우드 비용
가비아 클라우드 비용 ( 4vCore / 8GB )

1vCore 및 2GB의 경우 월 30,000원, 4vCore 및 8GB의 경우 월 140,500원의 비용이 발생하고 있습니다.

가비아 1분 서버 설치
가비아 1분 설치 서버

가비아에서 제공하는 1분 설치 서버를 사용한다면 4Core에 8GB의 경우 월 90,000원의 비용이 발생하고 있습니다.

3 티어 구성 시 비용

만약 웹 서비스를 3티어(Frontend 서버, Backend 서버, Database 서버)로 구성한다고 가정하면 동일하게 둘다 동일하게 월 9만원의 비용이 발생합니다. 서비스 운영 시점에 다다르면 서버를 추가/삭제하기보다는 인스턴스를 지속적으로 유지하는 경우가 많아 월 요금이 고정되는 경향이 있기에 월 요금으로 계산하였습니다.

  • v1Core 2GB 3개 인스턴스 필요 : 30,000 * 3 = 90,000
  • 4Core 8GB 서버 필요 : 90,000

이때 비용은 동일하나 1분 서버 설치가 더 가용성이 좋습니다. 그리고 만약 1분 서버 설치가 아닌 코로케이션 서버를 구매/임대하는 경우는 비용이 더 증가하나 더 많은 가용성을 확보할 수 있습니다.

코로케이션을 사용하는 경우

보통 코로케이션을 사용하는 경우가 더 적은 비용으로 더 많은 가용성을 확보할 수 있습니다. 다만, 코로케이션의 경우 업체에 따라서 약정 기간이 존재하는 경우가 있어 사용 기간에 대한 부담감이 존재할 수 있습니다. 또한, 동시 접속자 수가 갑작스럽게 증가하는 경우 대응하기 쉽지 않기 때문에 동시 접속자 수가 일정 수준으로 유지되는 경우에 유리합니다.

저는 웹 서비스 개발자들이 다 외부에서 개발하고 있기 때문에 개발 서버가 내부가 아닌 외부에 존재해야 합니다. 또한 데모 시스템을 제공하고 있기 때문에 데모 서버도 항시 운영되어야 합니다. 이 경우 실제 사용자 수는 많지 않고, 서버 인스턴스가 더 많이 필요하기 때문에 개발 서버와 데모 서버는 코로케이션 서버 한대로 NGINX와 Let’s Encrypt SSL 구성하여 내부에 FE 서비스 4개, BE 서비스 2개, MySQL을 설치하여 운영하고 필요에 따라서 웹 서비스를 올리고 내리고 하고 있습니다.

이때 발생하는 월 비용은 클라우드 비용 대비 50% 이상 절감됩니다.

하이브리드 방식

저는 운영 서버는 클라우드 기반으로 그외 나머지 서버는 모두 코로케이션 서버로 운영하고 있습니다.

운영 서버의 경우는 특정 이벤트에 따라서 2~3일 정도 동시 접속자 수가 평시 대비 100배 이상 증가하기 때문에, 그 기간만 로드밸런싱을 사용하여 서버를 추가했다가 제거합니다.

그리고 개발하고 서비스 하는 과정에 있어서 검증 서버 및 스테이징 서버 구성하는 데 있어 클라우드 서비스를 이용하고 있습니다.

  • 로컬 개발 : 각자 로컬에 개발
  • 개발 서버 : 코로케이션으로 운영 ( 기본적인 단위 테스트 진행 )
  • 검증 서버 : 개발 항목에 대한 검증 시 클라우드에서 인스턴스 생성 후 진행 ( 보통 2주 )
  • Staging 서버 : 검증 완료 후 실제 운영 서버와 동일한 형태의 클라우드 인스턴스 생성 후 진행 ( 2~3일 )
  • 운영 서버 : 클라우드에서 최소 스펙으로 로드밸런싱

마치며

클라우드가 관리 및 운영이 쉽고 편하기 때문에 클라우드를 선택하는 경우가 많이 있습니다. 전부다 클라우드를 사용하는 경우 비용이 많이 발생할 수 있기에 개발하는 형태 및 운영 형태에 따라서 클라우드가 아닌 코로케이션 서버를 직접 운영하는 것도 또 다른 대안이 될 수 있습니다.

합리적인 웹 서비스 운영이 되었으면 좋겠습니다.

참고 자료 및 관련 사이트

]]>
https://studioys.me/cloud-and-colocation-server-hosting/feed/ 0
MySQL 백업 및 복구 : mysqldump를 활용한 데이터 보호 https://studioys.me/how-to-backup-and-restore-mysql-using-mysqldump/ https://studioys.me/how-to-backup-and-restore-mysql-using-mysqldump/#respond Sun, 15 Dec 2024 03:28:55 +0000 https://studioys.me/?p=964 더 보기MySQL 백업 및 복구 : mysqldump를 활용한 데이터 보호]]> 데이터베이스는 서비스에서 가장 중요한 자산으로 예기치 못한 데이터 유실을 방지하기 위해 반드시 주기적으로 백업해야 합니다. 이번 포스팅에서는 mysqldump를 사용하여 MySQL 백업 및 복구 방법에 대해서 알아보도록 하겠습니다.

mysqldump란?

mysqldump는 MySQL에서 기본으로 제공하는 Command Line 기반 백업 도구입니다. 이 도구를 사용하여 원하는 데이터베이스의 데이터와 스키마를 텍스트 형식(SQL Script)으로 내보낼 수 있으며, 이를 통해 데이터베이스를 복구하거나, 데이터를 다른 서버로 이동할 수 있습니다.

mysqldump 설치 및 확인

mysqldump는 MySQL 클라이언트에 포함되어 있습니다. 글만약 MySQL이 설치가 안되어 있으면, MySQL 다운로드 페이지에서 원하는 버전을 다운로드 하여 설치하면 됩니다.

아래 명령어로 프로그램 설치 여부를 확인할 수 있습니다.

mysqldump --version

MySQL 백업 및 복구

MySQL 백업 및 복구 방법에 대해서 알아보겠습니다.

MYSQL 백업

단일 데이터베이스 백업

보통 단일 데이터베이스를 백업하기 때문에, 아래 명령을 사용하여 백업할 수 있습니다.

mysqldump -u [사용자명] -p [데이터베이스명] > [백업파일명].sql

# 예시
mysqldump -u root -p my_database > my_database_backup.sql

만약 원격지에 있는 데이터베이스를 백업하기 위해서는 -h 옵션을 추가하여 백업을 할 수 있습니다.

mysqldump -u [사용자명] -h [호스트] -p [데이터베이스명] > [백업파일명].sql

전체 데이터베이스 백업

서버에 있는 모든 데이터베이스를 백업하려면 --all-databases 옵션을 사용합니다.

mysqldump -u [사용자명] -p --all-databases > [백업파일명].sql

특정 테이블만 백업

특정 테이블만 선택적으로 백업할 수도 있습니다.

mysqldump -u [사용자명] -p [데이터베이스명] [테이블명1] [테이블명2] > [백업파일명].sql

기타 백업 옵션

백업 시 다양한 옵션이 존재합니다. 그 중 많이 사용하는 옵션은 다음과 같습니다.

  • –add-drop-database: 복구 시 기존 데이터베이스를 삭제하고 새로 생성합니다.
  • –add-drop-table: 복구 시 기존 테이블을 삭제하고 새로 생성합니다.
  • –routines: 저장 프로시저(Stored Procedure) 함수를 백업합니다.
  • –events: 이벤트 스케줄러를 백업합니다.

더 자세한 옵션은 MySQL 매뉴얼을 참고하면 됩니다.

MySQL 복구

백업한 파일을 사용해 데이터베이스를 복구하는 방법은 다음과 같습니다.

단일 데이터베이스 복구

아래 명령어로 단일 데이터베이스 백업 파일을 복구합니다.

mysql -u [사용자명] -p [데이터베이스명] < [백업파일명].sql

# 예시
mysql -u root -p my_database < my_database_backup.sql

전체 데이터베이스 복구

전체 데이터베이스 백업을 복구하려면 아래와 같이 사용합니다.

mysql -u [사용자명] -p < [백업파일명].sql

# 예시
mysql -u root -p < all_databases_backup.sql

백업 스크립트

아래는 정기적으로 MySQL 백업을 수행하는 간단한 Bash 스크립트입니다. 해당 파일을 cron에 등록하여 정기적으로 백업하는 것을 추천합니다.

#!/bin/bash
# MySQL 백업 스크립트

# 설정
USER="root"
PASSWORD="your_password"
DATABASE="my_database"
BACKUP_DIR="/path/to/backup"
DATE=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/${DATABASE}_backup_$DATE.sql"

# 백업 실행
mysqldump -u $USER -p$PASSWORD $DATABASE > $BACKUP_FILE

# 결과 확인
if [ $? -eq 0 ]; then
  echo "백업 성공: $BACKUP_FILE"
else
  echo "백업 실패"
fi

마치며

mysqldump는 MySQL 데이터베이스의 보호를 위한 강력한 도구입니다. 반드시 주기적으로 백업하고, 복구 테스트를 통해 백업된 파일의 유효성을 검증하는 것을 추천합니다.

]]>
https://studioys.me/how-to-backup-and-restore-mysql-using-mysqldump/feed/ 0
클라우드 서버에서 발생하는 React 빌드 오류 해결 방법 ( SIGKILL ) https://studioys.me/%ed%81%b4%eb%9d%bc%ec%9a%b0%eb%93%9c-%ec%84%9c%eb%b2%84%ec%97%90%ec%84%9c-%eb%b0%9c%ec%83%9d%ed%95%98%eb%8a%94-react-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98-%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-sigki/ https://studioys.me/%ed%81%b4%eb%9d%bc%ec%9a%b0%eb%93%9c-%ec%84%9c%eb%b2%84%ec%97%90%ec%84%9c-%eb%b0%9c%ec%83%9d%ed%95%98%eb%8a%94-react-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98-%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-sigki/#respond Thu, 05 Dec 2024 10:55:02 +0000 https://studioys.me/?p=953 더 보기클라우드 서버에서 발생하는 React 빌드 오류 해결 방법 ( SIGKILL )]]> React 앱을 개발한 후 배포를 위해 클라우드 서버에서 빌드를 진행해야 합니다. 이 때 로컬 개발 환경에서는 빌드가 잘 되는데 클라우드 서버에서 다음과 같은 React 빌드 오류가 발생하는 경우가 있습니다.

Creating an optimized production build...
/var/www/web/node_modules/react-scripts/scripts/build.js:19
  throw err;
  ^

RpcIpcMessagePortClosedError: Process 33108 exited [SIGKILL].
    at /var/www/web/node_modules/fork-ts-checker-webpack-plugin/lib/rpc/rpc-ipc/RpcIpcMessagePort.js:19:23
    at Generator.next (<anonymous>)
    at /var/www/web/node_modules/fork-ts-checker-webpack-plugin/lib/rpc/rpc-ipc/RpcIpcMessagePort.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (/var/www/web/node_modules/fork-ts-checker-webpack-plugin/lib/rpc/rpc-ipc/RpcIpcMessagePort.js:4:12)
    at ChildProcess.handleExit (/var/www/web/node_modules/fork-ts-checker-webpack-plugin/lib/rpc/rpc-ipc/RpcIpcMessagePort.js:18:42)
    at ChildProcess.emit (node:events:518:28)
    at ChildProcess._handle.onexit (node:internal/child_process:293:12) {
  code: null,
  signal: 'SIGKILL'
}

이 오류 메시지의 핵심은 SIGKILL 신호입니다. SIGKILL은 시스템이 프로세스를 강제로 종료할 때 발생하는 신호로, 다양한 원인이 있지만 위 상황에서는 주로 리소스 부족으로 인해 발생합니다. 클라우드 환경은 로컬 개발 환경에 비해 제한된 리소스를 사용하는데 이로 인하여 발생하는 경우가 많습니다.

React 빌드 오류 원인 분석

오류 메시지에서 fork-ts-checker-webpack-plugin/.../RpcIpcMessagePort.js:19:23 부분에 에러가 발생한 것을 볼 수 있습니다. 이러한 이유 때문에 fork-ts-checker-webpack-plugin 문제로 보고 설치 버전을 변경하거나 설정을 수정하는 경우가 많습니다. 하지만 실제로 해당 코드에서 문제가 발생하는 것이 아니라 프로세스가 비정상 종료되어 발생하는 문제입니다.

해당 위치의 코드는 아래와 같은데 프로세스가 종료될 때 실행되며, 오류를 처리하고 있는 코드입니다.

const handleExit = (code, signal) => __awaiter(this, void 0, void 0, function* () {
    closedError = new RpcIpcMessagePortClosedError_1.RpcIpcMessagePortClosedError(code
        ? `Process ${process.pid} exited with code "${code}" [${signal}]`
        : `Process ${process.pid} exited [${signal}].`, code, signal);
    errorListeners.forEach((listener) => {
        if (closedError) {
            listener(closedError);
        }
    });
    yield port.close();
});

해결 방법

이 문제를 해결하기 위해 다음과 같은 방법을 고려해볼 수 있습니다.

  1. 서버 메모리 증가: 클라우드 서버의 메모리가 부족해서 빌드 프로세스가 종료되었을 가능성이 큽니다. 따라서 서버의 메모리를 늘려서 빌드하는 것이 가장 직관적인 해결책입니다
  2. Docker 리소스 설정: Docker 환경에서 빌드를 진행 중이라면, Docker에 할당된 메모리와 CPU 자원을 충분히 확보해야 합니다. Docker의 기본 리소스 설정이 낮게 설정되어 있을 경우, 빌드 도중 메모리 부족으로 프로세스가 종료될 수 있습니다.
  3. 스왑 메모리 설정: 클라우드 서버에서 물리 메모리가 부족할 경우 스왑 메모리를 설정하여 메모리 부족 문제를 완화할 수 있습니다. 다만 스왑은 성능이 떨어질 수 있으므로 가능한 한 물리 메모리를 확보하는 것이 좋습니다.

결론

이 오류는 클라우드 서버의 리소스가 부족할 때 발생하는 경우가 많습니다. 문제 해결을 위해서는 서버의 메모리를 늘리거나, Docker 리소스를 조정하는 등의 방법을 통해 해결할 수 있습니다. 로컬 환경에서는 잘 동작하지만 클라우드 환경에서 문제가 발생한다면, 리소스 제한이 원인일 가능성을 항상 염두에 두어야 합니다.

참고 자료

]]>
https://studioys.me/%ed%81%b4%eb%9d%bc%ec%9a%b0%eb%93%9c-%ec%84%9c%eb%b2%84%ec%97%90%ec%84%9c-%eb%b0%9c%ec%83%9d%ed%95%98%eb%8a%94-react-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98-%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-sigki/feed/ 0
[윈도우 서비스] 03. 런처 서비스 https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-03-%eb%9f%b0%ec%b2%98-%ec%84%9c%eb%b9%84%ec%8a%a4/ https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-03-%eb%9f%b0%ec%b2%98-%ec%84%9c%eb%b9%84%ec%8a%a4/#respond Tue, 22 Oct 2024 09:21:22 +0000 https://studioys.me/?p=946 더 보기[윈도우 서비스] 03. 런처 서비스]]> 이전 포스팅을 참고하여 윈도우 서비스 기반으로 윈도우 부팅 시 Explorer 권한으로 특정 프로그램(여기서는 메모장)을 실행하는 런처 서비스를 만들어보겠습니다.

전체 소스 코드는 https://github.com/whalec-io/LauncherService 에서 받아볼 수 있습니다.

런처 서비스

런처는 특정 프로그램을 실행하는 것으로 윈도우 서비스를 만들어 윈도우 부팅 시 Explorer 권한으로 메모장을 실행 시켜보겠습니다. Windows Session이 추가 될 때 프로그램을 실행하거나 별도 프로세스에 대해서 WatchDog을 수행하기 위해서는 런처 서비스를 기반으로 추가로 작업하면 됩니다.

Service Main

ServiceMain에서는 work_thread를 생성하여 작업을 시작합니다. work_thread 생성 후 서비스 상태를 SERVICE_START_PENDING에서 SERVICE_RUNNING으로 변경하고, work_thread 종료 되면 서비스 상태를 SERVICE_STOPPED로 변경하여 서비스를 중지 상태로 변경합니다.

void ServiceMain(DWORD argc, LPCWSTR* argv)
{
  // ...
  
	UpdateServiceStatus(SERVICE_START_PENDING, NO_ERROR, 0, 3000);

	std::thread work_thread(WorkThread);

	UpdateServiceStatus(SERVICE_RUNNING, NO_ERROR, 0, 0);
	 
	work_thread.join();
	
	UpdateServiceStatus(SERVICE_STOPPED, NO_ERROR, 0, 0);
}

BOOL UpdateServiceStatus(
  DWORD current_state, 
  DWORD exit_code, 
  DWORD specific_exit_code, 
  DWORD wait_hint
  )
{
	static DWORD check_point = 1;
	SERVICE_STATUS service_status = { 0 };
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = current_state;
	service_status.dwControlsAccepted = (current_state == SERVICE_RUNNING) ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN) : 0;
	service_status.dwWin32ExitCode = (specific_exit_code == 0) ? exit_code : ERROR_SERVICE_SPECIFIC_ERROR;
	service_status.dwServiceSpecificExitCode = specific_exit_code;
	service_status.dwWaitHint = wait_hint;
	service_status.dwCheckPoint = ((current_state == SERVICE_RUNNING) || (current_state == SERVICE_STOPPED)) ? 0 : check_point++;

	return SetServiceStatus(service_state_handle, &service_status);
}

Work Thread

서비스가 상태 체크프로세스 실행을 수행합니다.
서비스 상태는 ServiceHandlerEx에서 SHUTDOWN 또는 STOP 이벤트 발생 시 전역 변수를 변경하고 Work Thread에서 해당 변수를 사용하여 서비스 중지 요청을 받을 경우 Thread를 종료합니다.

DWORD WINAPI ServiceHandlerEx(
  DWORD dwControl, 
  DWORD dwEventType, 
  LPVOID lpEventData, 
  LPVOID lpContext
)
{
	switch ( dwControl )
	{
	case SERVICE_CONTROL_SHUTDOWN:
	case SERVICE_CONTROL_STOP:
		UpdateServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 0, 0);
		is_running = FALSE;
		return NO_ERROR;
	default:
		break;
	}
	return NO_ERROR;
}

void WorkThread()
{
	const std::wstring process_path = L"C:\\windows\\system32\\notepad.exe";

	BOOL executed = FALSE;

	while ( is_running )
	{
		if ( !executed )
		{
			executed = RunAsExplorer(process_path.c_str());
		}

		Sleep(2000);
	}
}

전체 코드

런처 서비스 전체 코드는 다음과 같습니다.

#include <windows.h>
#include <winsvc.h>
#include <Tlhelp32.h>
#include <process.h>
#include <psapi.h>
#include <sddl.h>

#include <iomanip>
#include <iostream>
#include <map>
#include <mutex>
#include <thread>

#include "spdlog/sinks/basic_file_sink.h"
#include "spdlog/spdlog.h"

static WCHAR kServiceName[] = L"StudioYS Launcher Service";

SERVICE_STATUS_HANDLE service_state_handle;
BOOL is_running = FALSE;

std::mutex mutex_lock;

SC_HANDLE OpenSCM(DWORD desired_access)
{
	SC_HANDLE service_manager = OpenSCManagerW(NULL, NULL, desired_access);

	if ( service_manager == NULL )
	{
		spdlog::error("OpenSCManager Failed: 0x{:08x}", GetLastError());
	}

	return service_manager;
}

BOOL GetExplorerToken(HANDLE& hToken)
{
	spdlog::info("[GetExplorerToken] Start");

	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	if ( hSnapshot == INVALID_HANDLE_VALUE )
	{
		spdlog::error("[GetExplorerToken] CreateToolhelp32Snapshot Failed : 0x{:08x}", GetLastError());
		return FALSE;
	}

	DWORD explorerPID = 0;
	PROCESSENTRY32 pe;
	pe.dwSize = sizeof(PROCESSENTRY32);

	// Find explorer.exe process
	if ( Process32First(hSnapshot, &pe) )
	{
		do
		{
			if ( _wcsicmp(pe.szExeFile, L"explorer.exe") == 0 )
			{
				explorerPID = pe.th32ProcessID;
				spdlog::debug("[GetExplorerToken] Found Explorer PID : {}", explorerPID);
				break;
			}
		} while ( Process32Next(hSnapshot, &pe) );
	}

	CloseHandle(hSnapshot);

	if ( explorerPID == 0 )
	{
		spdlog::error("[GetExplorerToken] Explorer process not found.");
		return FALSE;
	}

	// Open process and get token
	HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, explorerPID);
	if ( hProcess == NULL )
	{
		spdlog::error("[GetExplorerToken] OpenProcess failed for Explorer PID: 0x{:08x}", GetLastError());
		return FALSE;
	}

	if ( !OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &hToken) )
	{
		CloseHandle(hProcess);
		spdlog::error("[GetExplorerToken] OpenProcessToken failed: 0x{:08x}", GetLastError());
		return FALSE;
	}

	CloseHandle(hProcess);
	spdlog::info("[GetExplorerToken] Successfully retrieved explorer token.");
	return TRUE;
}

BOOL RunAsExplorer(LPCWSTR lpApplicationName)
{
	HANDLE hToken;
	if ( !GetExplorerToken(hToken) )
	{
		spdlog::error("[RunAsExplorer] Failed to get explorer token");
		return FALSE;
	}

	HANDLE hNewToken;
	if ( !DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hNewToken) )
	{
		CloseHandle(hToken);
		spdlog::error("[RunAsExplorer] Failed to duplicate token: 0x{:08x}", GetLastError());
		return FALSE;
	}

	STARTUPINFOW si = { sizeof(STARTUPINFOW) };
	PROCESS_INFORMATION pi = { 0 };

	BOOL result = CreateProcessAsUserW(hNewToken, lpApplicationName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

	if ( result )
	{
		spdlog::info("[RunAsExplorer] Successfully created process.");
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
	}
	else
	{
		spdlog::error("[RunAsExplorer] Failed to create process: 0x{:08x}", GetLastError());
	}

	CloseHandle(hToken);
	CloseHandle(hNewToken);
	return result;
}

void WorkThread()
{
	spdlog::info("[WorkThread] Start");

	const std::wstring process_path = L"C:\\windows\\system32\\notepad.exe";

	BOOL executed = FALSE;

	while ( is_running )
	{
		if ( !executed )
		{
			if ( executed = RunAsExplorer(process_path.c_str()) )
			{
				spdlog::info("[WorkThread] run process");
			}
			else
			{
				spdlog::error("[WorkThread] Failed to run process");
			}
		}

		Sleep(2000);
	}

	spdlog::info("[WorkThread] Finish");
}

BOOL UpdateServiceStatus(DWORD current_state, DWORD exit_code, DWORD specific_exit_code, DWORD wait_hint)
{
	static DWORD check_point = 1;
	SERVICE_STATUS service_status = { 0 };
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = current_state;
	service_status.dwControlsAccepted = (current_state == SERVICE_RUNNING) ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN) : 0;
	service_status.dwWin32ExitCode = (specific_exit_code == 0) ? exit_code : ERROR_SERVICE_SPECIFIC_ERROR;
	service_status.dwServiceSpecificExitCode = specific_exit_code;
	service_status.dwWaitHint = wait_hint;
	service_status.dwCheckPoint = ((current_state == SERVICE_RUNNING) || (current_state == SERVICE_STOPPED)) ? 0 : check_point++;

	return SetServiceStatus(service_state_handle, &service_status);
}

DWORD WINAPI ServiceHandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
	switch ( dwControl )
	{
	case SERVICE_CONTROL_SHUTDOWN:
	case SERVICE_CONTROL_STOP:
		spdlog::info("SERVICE_CONTROL_STOP received");
		UpdateServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 0, 0);
		is_running = FALSE;
		return NO_ERROR;
	default:
		break;
	}
	return NO_ERROR;
}

void ServiceMain(DWORD argc, LPCWSTR* argv)
{
	spdlog::info("[ServiceMain] Start");

	service_state_handle = RegisterServiceCtrlHandlerEx(kServiceName, ServiceHandlerEx, NULL);
	if ( service_state_handle == NULL )
	{
		spdlog::error("[ServiceMain] RegisterServiceCtrlHandlerEx failed: 0x{:08x}", GetLastError());
		return;
	}

	UpdateServiceStatus(SERVICE_START_PENDING, NO_ERROR, 0, 3000);

	is_running = TRUE;
	std::thread work_thread(WorkThread);

	UpdateServiceStatus(SERVICE_RUNNING, NO_ERROR, 0, 0);
	 
	work_thread.join();
	UpdateServiceStatus(SERVICE_STOPPED, NO_ERROR, 0, 0);
}

void InstallMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_CREATE_SERVICE);
	if ( service_manager == NULL ) return;

	WCHAR file_path[MAX_PATH] = { 0 };
	GetModuleFileNameW(NULL, file_path, _countof(file_path));

	SC_HANDLE service_handle = CreateServiceW(
		service_manager, kServiceName, kServiceName, SERVICE_ALL_ACCESS,
		SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
		file_path, NULL, NULL, NULL, NULL, NULL);

	if ( service_handle == NULL )
	{
		spdlog::error("[InstallMyService] CreateService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	WCHAR description[] = L"StudioYS Launcher Service Description";
	SERVICE_DESCRIPTION sd = { description };
	ChangeServiceConfig2W(service_handle, SERVICE_CONFIG_DESCRIPTION, &sd);

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);

	spdlog::info("[InstallMyService] Service installed successfully.");
}

void UninstallMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_ALL_ACCESS);
	if ( service_manager == NULL ) return;

	SC_HANDLE service_handle = OpenService(service_manager, kServiceName, SERVICE_ALL_ACCESS);
	if ( service_handle == NULL )
	{
		spdlog::error("[UninstallMyService] OpenService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	if ( !DeleteService(service_handle) )
	{
		spdlog::error("[UninstallMyService] DeleteService Failed: 0x{:08x}", GetLastError());
	}

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);

	spdlog::info("[UninstallMyService] Service uninstalled successfully.");
}

void StartMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_ALL_ACCESS);
	if ( service_manager == NULL ) return;

	SC_HANDLE service_handle = OpenService(service_manager, kServiceName, SERVICE_ALL_ACCESS);
	if ( service_handle == NULL )
	{
		spdlog::error("[StartMyService] OpenService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	if ( !StartService(service_handle, 0, NULL) )
	{
		spdlog::error("[StartMyService] StartService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_handle);
		CloseServiceHandle(service_manager);
		return;
	}

	SERVICE_STATUS service_status;
	QueryServiceStatus(service_handle, &service_status);

	while ( service_status.dwCurrentState != SERVICE_RUNNING )
	{
		Sleep(service_status.dwWaitHint);
		QueryServiceStatus(service_handle, &service_status);
	}

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);
	spdlog::info("[StartMyService] Service started successfully.");
}

void StopMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_ALL_ACCESS);
	if ( service_manager == NULL ) return;

	SC_HANDLE service_handle = OpenService(service_manager, kServiceName, SERVICE_ALL_ACCESS);
	if ( service_handle == NULL )
	{
		spdlog::error("[StopMyService] OpenService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	SERVICE_STATUS service_status;
	QueryServiceStatus(service_handle, &service_status);

	if ( service_status.dwCurrentState != SERVICE_STOPPED )
	{
		if ( !ControlService(service_handle, SERVICE_CONTROL_STOP, &service_status) )
		{
			spdlog::error("[StopMyService] ControlService Failed: 0x{:08x}", GetLastError());
			CloseServiceHandle(service_handle);
			CloseServiceHandle(service_manager);
			return;
		}

		Sleep(2000);
	}

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);
	spdlog::info("[StopMyService] Service stopped successfully.");
}

int main(int argc, char* argv[])
{
	const std::string log_file_path = "log/Launcher Service.log";
	auto logger = spdlog::basic_logger_mt("basic_logger", log_file_path);
	spdlog::set_level(spdlog::level::trace);
	spdlog::set_default_logger(logger);
	spdlog::flush_on(spdlog::level::trace);

	spdlog::info("Main Start");

	SERVICE_TABLE_ENTRYW service_table[] = {
		{kServiceName, (LPSERVICE_MAIN_FUNCTIONW)ServiceMain},
		{nullptr, nullptr}
	};

	if ( argc >= 2 )
	{
		if ( _stricmp(argv[1], "--install") == 0 )
		{
			InstallMyService();
		}
		else if ( _stricmp(argv[1], "--uninstall") == 0 )
		{
			UninstallMyService();
		}
		else if ( _stricmp(argv[1], "--start") == 0 )
		{
			StartMyService();
		}
		else if ( _stricmp(argv[1], "--stop") == 0 )
		{
			StopMyService();
		}
	}
	else
	{
		StartServiceCtrlDispatcherW(service_table);
	}

	spdlog::info("Main Finish");
	return 0;
}

마치며

윈도우 서비스를 사용하여 런처 서비스를 구현해보았습니다. 만약 다른 권한으로 프로세스를 실행하거나, 와치독이 필요한 경우에는 추가로 수정하여 사용할 수 있습니다.

]]>
https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-03-%eb%9f%b0%ec%b2%98-%ec%84%9c%eb%b9%84%ec%8a%a4/feed/ 0