728x90
반응형

새로운 프로젝트를 위해 멋진 도메인을 구매했는데, support@my-domain.com 같은 메일 주소로 답장까지 보낼 수 있다면 얼마나 프로페셔널해 보일까요?

최근 Cloudflare에서 도메인을 구매하고 Gmail로 메일을 연동하는 과정에서 겪은 시행착오와 해결 방안을 공유합니다. 특히 "메일은 받아지는데 왜 발신은 안 되지?" 혹은 "왜 내 메일이 스팸함에 가지?"라는 고민을 가진 초보 개발자분들에게 이 글이 완벽한 가이드가 될 것입니다.

 

이전 글: https://fclipse.tistory.com/entry/cloudflare-custom-domain-email

 

[tip] 구매한 custom domain으로 이메일 보내고 받는 법 - cloudflare

Cloudflare에서 구매한 도메인을 Gmail과 연동하여 무료로 메일을 주고받는 방법은 크게 두 단계로 나뉩니다. 받는 메일은 Cloudflare의 이메일 라우팅(Email Routing) 기능을 사용하고, 보내는 메일은 Gmail

fclipse.tistory.com

 


1. 시작하며: 왜 수신은 되고 발신은 안 될까?

Cloudflare의 Email Routing은 기본적으로 '포워딩(전달)' 서비스입니다. 내 도메인으로 오는 메일을 내 Gmail로 던져주는 역할만 하죠. 하지만 Gmail에서 내 도메인 이름표를 달고 메일을 발송하려면, 메일을 대신 배달해줄 SMTP 서버(우체국)가 필요합니다.

우리는 별도의 유료 서비스 없이, 우리가 이미 가진 Gmail의 SMTP 서버를 활용하는 영리한 방법을 사용할 것입니다.


2. Gmail을 발신 서버로 설정하기

Step 1: Google 앱 비밀번호 생성

Gmail은 보안상 일반 비밀번호로 외부 연결을 허용하지 않습니다.

  1. Google 계정 설정 > 보안 > 2단계 인증을 활성화하세요.
  2. 하단의 앱 비밀번호 메뉴에서 'Cloudflare SMTP' 등의 이름으로 16자리 비밀번호를 생성해 저장해둡니다.

Step 2: Gmail에 다른 주소 추가

  1. Gmail 설정 > 계정 및 가져오기 > 다른 주소에서 메일 보내기를 클릭합니다.
  2. 내 도메인 메일(예: support@example.com)을 입력합니다. 이때 '별칭으로 간주'는 체크 해제하는 것을 추천합니다. 그래야 개인 메일과 업무용 메일이 명확히 분리됩니다.
  3. SMTP 서버 정보 입력:
    • SMTP 서버: smtp.gmail.com
    • 사용자 이름: 내 Gmail 주소 전체
    • 비밀번호: 아까 만든 16자리 앱 비밀번호

3. "신뢰할 수 없는 발신자" 경고 해결하기 (DNS 설정)

설정을 마쳤는데 메일을 보내보니 "보낸 사람을 확인할 수 없다"는 경고가 뜨나요? 이건 수신 측 서버가 여러분의 메일을 '사칭 메일'로 의심하기 때문입니다. 이를 해결하기 위해 Cloudflare DNS에 세 가지 조치를 취해야 합니다.

1) SPF (누가 보낼 수 있는가?)

내 도메인을 대신해 메일을 보낼 수 있는 서버 목록을 등록합니다.

  • 기존: v=spf1 include:_spf.mx.cloudflare.net ~all
  • 수정: v=spf1 include:_spf.mx.cloudflare.net include:_spf.google.com ~all
    (Google 서버도 내 도메인을 쓸 수 있다고 허가해주는 것입니다.)

2) DKIM (메일이 변조되었는가?)

메일에 디지털 서명을 입히는 과정입니다. Cloudflare에서 제공하는 기본 DKIM 레코드를 유지하세요.

3) DMARC (인증 실패 시 어떻게 할 것인가?)

최근 보안 트렌드에서 가장 중요한 레코드입니다. 이 레코드가 없으면 스팸 처리될 확률이 높습니다.

  • 타입: TXT
  • 이름: _dmarc
  • 값: v=DMARC1; p=none;
    (p=none은 초기 설정 시 메일 유실을 방지하면서 모니터링하기 가장 안전한 값입니다.)

4. 초보 개발자를 위한 디버깅 팁: PowerShell 활용

설정이 잘 되었는지 브라우저에서만 확인하지 말고, 터미널을 열어 직접 질의해 보세요.

# SPF 레코드 확인
Resolve-DnsName -Name 내도메인 -Type TXT

# DMARC 레코드 확인
Resolve-DnsName -Name _dmarc.내도메인 -Type TXT

결과 값에 내가 입력한 include:_spf.google.com이나 v=DMARC1이 보인다면 설정 성공입니다!


5. 마치며: 인프라의 이해가 성장을 만듭니다

의의: 단순히 클릭 몇 번으로 끝낼 수도 있지만, SMTP, SPF, DKIM, DMARC라는 개념을 이해하는 과정은 네트워크와 보안 인프라를 이해하는 훌륭한 밑거름이 됩니다.

처음 설정을 마친 뒤 경고 문구가 바로 사라지지 않더라도 당황하지 마세요. DNS 전파와 수신 서버의 신뢰도 업데이트에는 시간(최대 24시간)이 필요합니다.


궁금한 점이나 설정 중 막히는 부분이 있다면 댓글로 남겨주세요!

#Cloudflare #Gmail연동 #커스텀도메인 #이메일보안 #SMTP #DMARC

728x90
반응형
LIST
728x90
반응형

1. 개요

  • 현상: pmpt.sh 도메인의 A 레코드를 변경했음에도 불구하고, 루트 도메인 접속 시 이전 사이트(daybreaker42.com)로 연결됨. 반면, 특정 하위 경로(pmpt.sh/prompt/...)는 정상적으로 새 사이트에 연결되는 불일치 발생.
  • 확인 결과: 브라우저의 개발자 도구 내 'Disable Cache' 옵션 활성화 시 정상 동작 확인.

2. 기술적 원인 분석 (Root Cause Analysis)

2.1. HTTP 리다이렉트 캐싱 (Browser-Level)

가장 핵심적인 원인은 브라우저가 이전 서버에서 수신했던 HTTP 301 (Moved Permanently) 응답을 로컬에 영구 캐싱했기 때문입니다.

  • 메커니즘: 브라우저는 301 응답을 받으면 해당 도메인과 목적지 URL의 매핑 정보를 Disk Cache에 저장합니다.
  • 동작 방식: 이후 동일 도메인(pmpt.sh) 접속 시, 네트워크 질의(DNS 조회 및 TCP 핸드셰이크)를 생략하고 즉시 캐싱된 타겟으로 리다이렉트합니다.
  • 경로별 차이: 루트 도메인은 자주 접속하여 캐시가 존재했으나, 특정 UUID가 포함된 하위 경로는 캐시된 매핑 데이터가 없어 서버에 직접 요청을 보냈고, 이 과정에서 업데이트된 DNS 정보를 참조하여 새 사이트로 연결되었습니다.

2.2. DNS TTL (Time To Live) 및 전파 지연

  • 원인: 이전 DNS 레코드의 TTL 설정이 길게 남은 경우, ISP(인터넷 서비스 제공자)의 Recursive DNS 서버가 여전히 예전 IP를 반환할 수 있습니다.
  • 영향: 브라우저 캐시가 비워져 있더라도 시스템 레벨의 DNS 캐시가 살아있다면 구형 IP로의 TCP 연결이 지속됩니다.

3. 아키텍처적 고려사항 및 대응 전략

3.1. 구현 레벨의 리다이렉트 최적화

프로덕션 환경에서는 영구적인 도메인 이전이 확정되기 전까지는 302 Found 또는 307 Temporary Redirect를 사용하는 것이 권장됩니다.

3.2. 인프라 설정 모범 사례

  • TTL 사전 관리: 레코드 변경 24시간 전에 TTL을 300s(5분) 이하로 낮추어 전파 속도를 확보합니다.
  • HSTS 주의: 만약 이전 서버에서 Strict-Transport-Security 헤더를 운용했다면, 브라우저는 무조건 HTTPS 접속을 시도하며 이 과정에서 이전 서버의 인증서 정보를 대조할 수 있어 도메인 이전 시 충돌 위험이 있습니다.

4. 결론 및 향후 조치

  • 현재 상태: 브라우저 캐시 무효화(Disable Cache)를 통해 로컬 환경의 문제가 해결되었음을 확인했습니다.
  • 일반 사용자 대응: 일반 사용자의 경우 수동으로 캐시를 비우기 어려우므로, 서버 측에서 Cache-Control: no-store 헤더를 명시하거나 쿼리 스트링(e.g., ?v=2)을 활용한 캐시 버스팅(Cache Busting)을 검토할 수 있습니다.
  • 모니터링: dig 또는 nslookup 도구를 사용하여 외부망에서의 DNS 전파 상태를 지속적으로 확인하는 것이 필요합니다.

참조: RFC 7231 (HTTP/1.1 Semantics and Content), RFC 1034 (Domain Names)

728x90
반응형
LIST
728x90
반응형
Cloudflare에서 구매한 도메인을 Gmail과 연동하여 무료로 메일을 주고받는 방법은 크게 두 단계로 나뉩니다. 받는 메일은 Cloudflare의 이메일 라우팅(Email Routing) 기능을 사용하고, 보내는 메일은 Gmail의 SMTP 서버앱 비밀번호를 이용해 설정합니다.

1단계: 메일 받기 설정 (Cloudflare Email Routing)

Cloudflare 대시보드에서 내 도메인으로 오는 메일을 내 기존 Gmail 계정으로 전달하는 설정입니다.
  1. Email Routing 활성화: Cloudflare 대시보드에서 해당 도메인을 선택한 후 왼쪽 메뉴의 Email > Email Routing으로 들어갑니다.
  2. 대상 주소 등록: Destination addresses 탭에서 메일을 실제 수신할 본인의 Gmail 주소를 입력하고 인증 메일을 확인합니다.
  3. 라우팅 규칙 생성: Routing rules에서 me@yourdomain.com과 같은 커스텀 주소를 만들고, 이를 방금 인증한 Gmail 주소로 전달(Forward to)하도록 설정합니다.
  4. DNS 레코드 자동 추가: 설정 과정 중 'Add records and enable' 버튼을 눌러 필요한 MX 및 TXT(SPF) 레코드를 자동으로 DNS에 등록합니다.

2단계: 메일 보내기 설정 (Gmail SMTP 연동)

Gmail 인터페이스에서 내 커스텀 도메인 주소를 발신인으로 설정하는 과정입니다.
  1. Google 앱 비밀번호 생성:
    • Google 계정 설정에서 2단계 인증이 활성화되어 있어야 합니다.
    • 보안 메뉴에서 앱 비밀번호(App Passwords)를 검색하여 '메일'용 비밀번호를 생성하고 생성된 16자리 코드를 복사해둡니다.
  2. Gmail에 계정 추가:
    • Gmail 설정(톱니바퀴) > 모든 설정 보기 > 계정 및 가져오기 탭으로 이동합니다.
    • 다른 주소에서 메일 보내기 섹션에서 다른 이메일 주소 추가를 클릭합니다.
  3. SMTP 서버 정보 입력:
    • 이메일 주소: Cloudflare에서 만든 커스텀 도메인 주소(예: me@yourdomain.com)를 입력합니다.
    • SMTP 서버: smtp.gmail.com.
    • 포트: 587 (TLS 사용).
    • 사용자 이름: 본인의 전체 Gmail 주소.
    • 비밀번호: 위에서 복사한 16자리 앱 비밀번호를 입력합니다.
  4. 인증: Gmail로 발송된 확인 코드를 입력하여 설정을 완료합니다.

3단계: SPF 레코드 수정 (스팸 방지)

메일이 스팸함으로 빠지지 않도록 Cloudflare DNS 설정에서 기존 SPF 레코드를 수정해야 합니다.
  • Cloudflare DNS 메뉴에서 기존 v=spf1 include:_spf.mx.cloudflare.net ~all 레코드를 찾아 아래와 같이 수정합니다.
    • 수정 후: v=spf1 include:_spf.google.com include:_spf.mx.cloudflare.net ~all
설정이 완료되면 Gmail에서 메일을 쓸 때 보내는 사람 목록에서 본인의 커스텀 도메인 주소를 선택할 수 있습니다.

 

 

 

다음 글: https://fclipse.tistory.com/entry/cloudflare-custom-domain-email-2

 

[tip] 구매한 custom domain으로 이메일 보내고 받는 법 2

새로운 프로젝트를 위해 멋진 도메인을 구매했는데, support@my-domain.com 같은 메일 주소로 답장까지 보낼 수 있다면 얼마나 프로페셔널해 보일까요?최근 Cloudflare에서 도메인을 구매하고 Gmail로 메

fclipse.tistory.com

 

728x90
반응형
LIST
728x90
반응형

개요

windows powershell에선 .sh 파일을 실행하지 못한다. 하지만 이를 실행할 수 있는 방법이 있다.

환경

windows 11
windows powershell 7.6.0

wsl --version
WSL 버전: 2.6.3.0
커널 버전: 6.6.87.2-1
WSLg 버전: 1.0.71
MSRDC 버전: 1.2.6353
Direct3D 버전: 1.611.1-81528511
DXCore 버전: 10.0.26100.1-240331-1435.ge-release
Windows 버전: 10.0.26200.8246

git --version
git version 2.51.0.windows.2

방법

  1. git 설치
  2. 다음 실행파일을 이용해 .sh 파일을 실행
    & "C:\Program Files\Git\bin\sh.exe" <실행파일 경로>
728x90
반응형
LIST
728x90
반응형


최근 Playwright를 이용해 자동화 봇을 만들고 있었습니다. 로컬이나 실서버 Ubuntu의 .venv 환경에서는 제대로 동작하는 봇이, 유독 도커(Docker) 컨테이너 안으로만 들어가면 로그인 단계에서 Timeout 에러를 뱉으며 멈춰버리는 현상을 겪었습니다.

결론부터 말하자면, 이 문제는 스크립트의 논리 오류가 아니라 도커 환경의 리소스 제한과 브라우저 렌더링 방식의 차이 때문이었습니다. 저와 같은 삽질을 반복하지 않으시길 바라며 해결 과정을 정리합니다.

1. 증상: "로컬에선 되는데 왜 도커만 이래?"

로컬에서 실행할 때(특히 창 모드)는 아무 문제 없던 코드가 도커 내 Headless 환경에서는 다음과 같은 에러를 발생시켰습니다.

Error in automation: Page.click: Timeout 30000ms exceeded.
... <div role="dialog" ... intercepts pointer events

로그를 뜯어보니, 로그인 버튼을 누르려는데 정해진 시간 내에 버튼이 나타나지 않거나, 투명한 다이얼로그(팝업) 레이어가 버튼 위를 덮고 있어 클릭 이벤트가 전달되지 않는 상태였습니다.

2. 원인 분석

핵심 원인 1: 공유 메모리(/dev/shm)의 부족

도커 컨테이너는 기본적으로 공유 메모리 공간을 64MB로 극단적으로 제한합니다. 하지만 Chromium 브라우저는 복잡한 페이지를 렌더링할 때 이보다 훨씬 많은 메모리를 사용합니다. 메모리가 부족하면 화면 렌더링이 꼬이거나, 닫혔어야 할 팝업이 그대로 남아 버튼을 가리게 됩니다.

핵심 원인 2: 샌드박스 보안 충돌

도커라는 격리 공간 안에서 브라우저가 또 자체 샌드박스를 만들려다 보니 렌더링 스레드가 비정상적으로 작동하는 경우가 많습니다.

핵심 원인 3: Headless 환경의 UI 변형

환경 차이로 인해 평소엔 안 뜨던 보안 안내 팝업이나 공지사항이 도커 환경에서만 튀어나올 수 있습니다. 이때 Playwright의 일반적인 click()은 가려진 요소를 누르지 못하고 타임아웃에 빠집니다.

3. 해결 방안

Step 1. docker-compose 설정 (메모리 해방)

도커 컨테이너가 브라우저 렌더링을 위해 충분한 공유 메모리를 쓸 수 있도록 shm_size를 늘려줍니다.

# docker-compose.yml
services:
  library-bot:
    build: .
    shm_size: '2gb'  # 64MB의 족쇄를 풀어줍니다.

Step 2. 브라우저 실행 옵션(Launch Args) 추가

도커 환경에 최적화된 실행 옵션을 추가합니다. --no-sandbox--disable-dev-shm-usage를 많이 사용합니다.

# server.py
self.browser = await self.playwright.chromium.launch(
    headless=True,
    args=[
        "--no-sandbox",             # 샌드박스 비활성화
        "--disable-dev-shm-usage"  # /dev/shm 대신 /tmp 사용
    ]
)

Step 3. 강제 클릭 옵션 (force=True) 적용

팝업이 버튼을 가리고 있더라도, 요소를 찾았다면 무조건 이벤트를 발생시키도록 force=True 옵션을 부여합니다.

# 일반 클릭 대신 강제 클릭 사용
await page.click("button", force=True)
await page.click('button', force=True)

4. 결과: "Success!"

이렇게 설정을 바꾸고 나니, 기존 문제들이 모두 해결되었고, 정상적으로 동작하는 걸 확인할 수 있었습니다. 이번 일로 인해 잘 모르는 환경에선 먼저 어떻게 진행해야 하는지에 대한 기초 지식이 정말 중요하다고 느꼈습니다.


요약

  1. 도커의 shm_size를 늘리자.
  2. --no-sandbox 옵션을 잊지 말자.
  3. 방해물이 있다면 force=True 사용.

참고자료

Selenium 자동화를 위한 Chrome 브라우저 실행 옵션 총정리

도움이 되셨다면 공감 부탁드립니다! 궁금한 점은 댓글로 남겨주세요. :)

728x90
반응형
LIST
728x90
반응형

폴더 정리 방법

1. 5레벨 이하로 파일 구조 설정
2. 폴더 / 파일 앞에 두 자리 숫자로 넘버링
- 섞일 일 없고
- 작업중인 폴더 맨 위로 보내기도 쉬움
- 예외 존재
- 01~99까지

- 99는 아카이브 폴더 - 프로젝트 끝났거나 사용할 일 없다면 99에 넣어주기
작업중인 폴더 많을때 - 눈에 잘 안들어옴 - 아카이브 만들기 (필요할 때만)

3. 개인 ssd 구조
1) 개인 - 개인적 파일들 - 사진, 영상, 공부 자료 등
2) 업무 - 비즈니스, 유튜브, 영상 / 외주 작업 폴더
3) 템플릿 - 효과음, 음성, 폰트 등 모아둔 폴더 / 사무직이라면 필요 없을수도? - 구글 드라이브로 구조 일치

4. quick share 폴더 - 작업중인 폴더 공유 쉽게 - 구글 드라이브에서 사용, 공유중인거 잊지 않을 수 있음

5. 규칙 위배 - 1) 갤러리엔 연도별

큰 틀을 따르되, 상황에 따라 유동적으로 관리

사진 / 영상 등 - 시간순으로 정리하는게 더 편함
외주도 분기별로 네이밍하는게 나음

백개 이상 넘어가는 경우 3자리로

별로 중요 x - 규칙 x

6. 네이밍 규칙 정하기
1) 이름에 날짜 포함
- 연도
- 연도_분기
- 연도_월
- 연도_월_날짜
-> 파일 유형에 따라 나뉨
2) 가나다 순으로

과한 최적화는 효율을 저하 - 한 가지 시스템을 정해 규칙을 고수하기

바로가기 등 활용

참고자료
떼끄 - https://youtu.be/pyj49VW6BFg?si=Fg64grkMrtKxo_dy
jeff su - https://youtu.be/MM-MPS57qKA?si=XDMZbqJ3MFJecIQE

728x90
반응형
LIST
728x90
반응형

안녕하세요! 오늘은 윈도우와 리눅스(Xubuntu 등)를 함께 사용하는 분들이 가장 흔히 겪는 불편함 중 하나인 "부팅 시마다 나타나는 BitLocker(장치 암호화) 복구 키 입력 창" 문제를 해결하는 방법을 정리해 보겠습니다.

1. 왜 자꾸 복구 키를 입력하라고 하나요?

윈도우의 장치 암호화(BitLocker) 기능은 보안 칩(TPM)을 통해 시스템의 안전을 확인합니다. 리눅스 설치를 위해 BIOS에서 Secure Boot(보안 부팅)를 끄거나 부팅 순서를 바꾸면, TPM은 "시스템이 변조되었다"고 판단하여 데이터 보호를 위해 복구 키를 요구하게 됩니다.


2. 보안적으로 암호화를 해제해도 괜찮을까?

결론부터 말씀드리면, 사용 환경에 따라 다릅니다.

  • 해제해도 괜찮은 경우: 노트북을 집에서만 사용하거나, 분실 위험이 낮은 데스크탑인 경우.
  • 유지해야 하는 경우: 카페, 학교 등 외부 이동이 잦은 노트북. (분실 시 SSD를 떼어가도 데이터를 볼 수 없게 보호해 줍니다.)

불편함을 없애는 방법은 두 가지입니다. 보안을 유지하며 해결하거나, 아예 기능을 끄는 것입니다.


3. 해결 방법 A: 보안 부팅(Secure Boot) 켜고 암호화 유지하기 (추천)

보안과 편의성을 모두 잡는 방법입니다. 최신 리눅스(Xubuntu 25.10 등)는 보안 부팅을 지원합니다.

  1. 리눅스에서 MOK 등록 준비:
  • 터미널에서 다음 명령어 입력 후 일회용 비밀번호 설정.
sudo update-secureboot-policy --enroll-key
  1. BIOS 설정:
  • 재부팅 시 BIOS에 진입하여 Secure Boot[Enabled]로 변경.
  1. MOK 등록(파란 화면):
  • 리눅스로 부팅될 때 파란 화면(Perform MOK management)이 뜨면 Enroll MOK -> Continue -> Yes를 선택하고 아까 정한 비밀번호 입력.
  1. 윈도우에서 암호화 갱신:
  • 윈도우로 부팅(마지막 키 입력) 후, [설정] > [장치 암호화]에서 기능을 잠시 [끄기] 했다가 다시 [켜기]를 합니다.
  • 이제 시스템이 "보안 부팅이 켜진 상태"를 정상으로 기억하여 더 이상 키를 묻지 않습니다.

4. 해결 방법 B: 장치 암호화 기능 완전히 끄기

설정이 복잡하고 보안보다는 편의성이 중요하다면 암호화 자체를 해제하면 됩니다.

  1. 윈도우 [설정] > [개인 정보 및 보안] > [장치 암호화]로 이동합니다.
  2. [장치 암호화] 스위치를 [끄기]로 바꿉니다.
  3. 복호화 완료까지 대기: 드라이브 용량에 따라 시간이 걸릴 수 있습니다. 완료되면 자물쇠 아이콘이 사라지며, 어떤 환경에서 부팅해도 더 이상 키를 묻지 않습니다.

마치며

리눅스와 윈도우를 오가는 환경에서는 보안 부팅(Secure Boot) 설정이 BitLocker와 긴밀하게 연결되어 있습니다. 가장 권장하는 방법은 방법 A를 통해 보안을 유지하는 것이지만, 개인용 PC이고 매번 뜨는 파란 창이 너무 번거롭다면 방법 B로 해제하는 것도 합리적인 선택입니다.

궁금한 점이 있다면 댓글로 남겨주세요!

728x90
반응형
LIST
728x90
반응형

개요

그럼 이제 본격적으로 개발을 해봐야겠죠.

vscode, android studio, git, curl, flutter 등등 플러터 개발에 필요한 필수 프로그램을 설치하고 개발에 들어갔습니다.
그런데, 일단 문제가 여럿 생겼습니다.

문제

  1. vscode에서 keytools을 인식 못한다는 에러가 자꾸 뜸 - 일부 extension github 로그인이 유지가 안되는 문제
  2. android studio 설치시 공식 홈페이지에 있는 라이브러리가 설치가 안되는 문제
  3. android studio에서 생성한 에뮬레이터가 vscode에서 인식되지 않는 문제
  4. gradle language server initialize가 끝나지 않는 문제
  5. android studio 에뮬레이터가 불안정한 문제 - 2에서 설치를 제대로 하지 못한게 문제인지, 아니면 리눅스 환경에서 돌리는게 불안정해서인지를 몰라도, 에뮬레이터에서 앱을 돌리려 하면 제대로 돌아가지 않는다는 문제가 있었습니다.
  6. bitlocker문제 - 리눅스 설치를 하던 중, safe booting을 꺼야 하는 일이 있었는데 이때 기존 윈도우 디스크에 bitlocker가 설정되어 있어서 윈도우 부팅시마다 키를 입력해줘야 했습니다.

하지만, 이 모든 문제를 겪고도 '속도가 빠르다' 와 '램 사용량이 적다'는 확실한 장점이 있었기에, 리눅스를 선택했습니다.

해결

무엇보다 위 5가지의 문제는 다음 방법으로 해결할 수 있었습니다.

  1. vscode에서 keytools을 인식 못한다는 에러 -> 무시한다
  2. android studio 설치시 공식 홈페이지에 있는 라이브러리가 설치가 안되는 문제 -> 이건 gemini한테 물어봐서 상위 버전 library을 설치함으로서 얼추 해결했습니다. (설치 안해도 돌아가긴 합니다)
  3. android studio에서 생성한 에뮬레이터가 vscode에서 인식되지 않는 문제 -> android studio 켜서 에뮬레이터를 키거나, 5.의 해결책 사용
  4. gradle language server initialize가 끝나지 않는 문제 -> 무시한다
  5. android studio 에뮬레이터가 불안정한 문제 -> android studio에서 wifi 디버깅 기능을 사용해서 다른 기기에서 앱을 돌리니 잘 돌아감.
  6. bitlocker문제 - xubuntu도 safe booting을 지원해서 설치가 완료된 이후, 관련 설정을 마무리했습니다. 다시 설정을 키고, 리눅스에서 mok 서명을 확인한 뒤, 윈도우에 들어가 bitlocker 설정을 껐다가 키면 완료됩니다.

다른 문제들은 개발에 있어 직접적인 문제는 없었지만, 5번 문제가 앱 개발에 있어 상당히 치명적이었습니다. 하지만 다행히 android studio에서 같은 wifi 내 안드로이드 기기를 무선으로 연결해 디버깅할 수 있게 하는 기능을 지원해서 해당 방법으로 다른 기기에서 앱을 돌리니 잘 돌아가는 모습을 볼 수 있었습니다.

추가 적용한 점들

Xubuntu(Xfce 데스크탑 환경)에서 해상도를 기본값으로 맞췄는데 아이콘이나 글씨가 너무 작게 보이는 문제

1. 폰트 DPI 조정을 통한 크기 조절 (가장 효과적)

배율을 1.0(기본값)으로 되돌린 후, DPI 값을 높여서 전체적인 UI 요소와 텍스트 크기를 키우는 방법입니다.

  1. 설정 관리자 > 모양새(Appearance) > 폰트(Fonts) 탭으로 이동합니다.
  2. Custom DPI setting 체크박스를 활성화합니다.
  3. 기본값인 96을 144 로 설정.
  • 숫자가 커질수록 글자와 창의 크기가 선명하게 커집니다.

2. 창 관리자(Window Manager) 테마 변경 - 적용 x

DPI를 높여도 창의 제목 표시줄이나 버튼이 작다면 'HiDPI'용 테마를 선택해야 합니다.

  1. 설정 관리자 > 창 관리자(Window Manager)로 이동합니다.
  2. 스타일 탭에서 이름 뒤에 -hdpi가 붙은 테마(예: Default-hdpi)를 선택합니다.
  3. 이렇게 하면 창 테두리와 최소화/최대화 버튼이 고해상도에 맞게 커집니다.

3. 아이콘 크기 및 테마 수정 - 패널 높이 수정

아이콘이 여전히 작다면 개별적으로 크기를 지정할 수 있습니다.

  • 패널(작업표시줄) 아이콘: 패널 우클릭 > 패널 기본 설정 > 모습 탭에서 '아이콘 크기'를 수동으로 조절하거나 '행 크기'를 키웁니다.
  • 바탕화면 아이콘: 바탕화면 우클릭 > 바탕화면 설정 > 아이콘 탭에서 '아이콘 크기' 숫자를 직접 키웁니다.

4. 브라우저 및 개별 앱 배율 (보너스) - 필요 x

시스템 전체 배율을 1.0으로 뒀기 때문에 웹 브라우저가 작게 보일 수 있습니다.

  • Firefox/Chrome: 설정에서 '기본 페이지 확대/축소'를 125% 또는 150%로 설정하면 화질 저하 없이 깨끗하게 볼 수 있습니다.

클립보드 보이게 하는 기능

[추가 설정] CopyQ 클립보드 매니저 단축키 지정 및 자동 실행

Xubuntu(XFCE) 환경에서 효율적인 클립보드 관리를 위해 CopyQ를 설치한 후, 단축키로 목록을 즉시 호출하고 시스템 시작 시 자동 실행되도록 설정하는 방법입니다.

1. 클립보드 호출 단축키 설정

윈도우의 Win + V처럼 특정 조합(예: Super + V)으로 클립보드 히스토리를 즉시 띄울 수 있습니다.

  • 설정 경로: 설정(Settings)키보드(Keyboard)응용 프로그램 바로가기
  • 명령어 추가: + 추가 버튼 클릭 후 copyq show 입력
  • 단축키 지정: 원하는 키 조합(예: Super + V) 입력 시 즉시 등록

2. 시스템 시작 시 자동 실행 설정

매번 수동으로 실행할 필요 없이 부팅과 동시에 CopyQ가 활성화되도록 설정합니다.

  • 설정 경로: 설정(Settings)세션 및 시작(Session and Startup)

  • 항목 추가: 응용 프로그램 자동 시작 탭에서 + 추가 클릭

  • 내용 입력: * 이름: CopyQ

  • 명령어: copyq

  • 확인: 리스트에 체크되어 있는지 확인 후 저장

zsh 및 oh my zsh 설정

참고 - https://poki.tistory.com/entry/LinuxTerminal-Linux%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-Zsh-Oh-My-Zsh-%EC%84%A4%EC%B9%98-%EA%B0%80%EC%9D%B4%EB%93%9C

총평

이번에 xubuntu을 디스크에 설치하고, 개발을 해보면서 느낀 점은 다음과 같습니다.

  1. 속도와 사용성은 반비례할수밖에 없다. 이걸 해결하려면 돈을 쓰면 된다. (애플)
  2. 윈도우가 사용성은 정말 좋은 편이다. 애플보다 밀린다고 해서 무시할게 아니다. 익숙함이 참 무섭다.
  3. 리눅스는 역시 빠르다. 또한 snap 명령어로 프로그램을 설치하면서 명령어 한번으로 설치되는게 편리하다고 느꼈습니다.
  4. 리눅스에서 설정이 꼬이거나 오류가 나면 문제가 커진다 - 사용자가 적기 때문에 아직 해결되지 않았거나 인터넷에 찾아봐도 해결책이 없을 수 있음. 하지만 AI의 발달로 관련 정보를 찾기 매우 수월해져서 저와 같은 초보 개발자들도 설정을 하기 쉬워진건 있습니다.

또한 이후 다른 개발을 할 때 리눅스가 필요한 경우가 있다면 해당 환경을 사용할 것 같습니다. wsl이 있어 이를 우선적으로 사용은 하겠지만, 역시 빠른게 좋은 것 같습니다.

728x90
반응형
LIST
728x90
반응형

개요


Google Play store에 app을 올리기 위해선 key로 서명을 해야 한다.
평소에는 android studio을 이용해 generate signed appbundle 기능으로 android studio에서 signed app bundle을 만들었다.

그런데 flutter에서 .env파일 사용 시 dotenv을 사용하면 apk에서 .env 값을 볼 수 있다는 글을 보았다.
따라서 flutter_dotenv패키지를 전면 교체 후 build시에 커멘드라인에서 주입하는 방식으로 교체했다.

--dart-define-fron-file=.env

이걸 build 명령시 인자에 추가해주면 더이상 .env을 asset으로 포함시키지 않아도 된다.

명령어는 다음과 같다.

# run flutter
flutter run --dart-define-from-file=.env

# run without debug
flutter run --release --dart-define-from-file=.env

# build appbundle
flutter build appbundle --release --dart-define-from-file=.env

vscode launch.json 설정

또한 vscode 단축키를 눌러 (ctrl + shift + f5) 빌드를 자주 하는데, 이때도 추가로 인자를 넣어줘야 한다.
다음과 같이 설정해서 .env 인자를 추가할 수 있다.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Flutter",
      "request": "launch",
      "type": "dart",
      "args": [
        "--dart-define-from-file=.env"
      ]
    },
    {
      "name": "Flutter (Run Without Debugging)",  // 새 구성 추가
      "request": "launch",
      "type": "dart",
      "noDebug": true,  // 디버그 모드 비활성화
      "args": [
        "--dart-define-from-file=.env"
      ]
    }
  ]
}

참고 자료

Flutter에서 환경변수 설정하기

Flutter 앱 배포 가이드: 환경변수와 함께 Android & iOS 스토어에 배포하기

문제 상황

1. flutter build appbundle --release --dart-define-from-file=.env으로 빌드시 google play에 appbundle을 업로드하면 서명이 되지 않았다고 뜬다.
사진에서도 '업로드된 모든 번들에 서명해야 합니다' 라고 에러가 떠있는걸 볼 수 있다.

app/build.gradle.kts 파일도 잘 설정을 해줬었는데 이때는 뭐가 문제였는지 몰랐다.

 

2. 또한 기존에는 android studio로 appbundle을 빌드했는데, 이제는 --dart--define-from-file 커맨드가 적용되지 않아 .env파일 주입을 할 수 없었다. (빌드는 되지만, appbundle 업로드 후 test을 받으면 .env파일이 빠져있다.)

 

3. 또한 flutter 공식 홈페이지에서도 android studio을 이용하는 방법 말고는 제대로 appbundle 서명 및 빌드 방법을 알려주지 않는다.

Build and release an Android app

 

Build and release an Android app

How to prepare for and release an Android app to the Play store.

docs.flutter.dev

 

앱 서명  |  Android Studio  |  Android Developers

 

앱 서명  |  Android Studio  |  Android Developers

앱 서명 및 보안과 관련된 중요한 개념을 알아보고, Android 스튜디오를 사용하여 Google Play에 출시하기 위해 앱에 서명하는 방법과 Play 앱 서명을 선택하는 방법을 알아보세요.

developer.android.com

 

해결 전 build.gradle.kts (일부)

...
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
...

android {
    ...

    signingConfigs {
        create("release") {
            if (keystorePropertiesFile.exists() && keystoreProperties["storeFile"] != null) {
                keyAlias = keystoreProperties["keyAlias"] as String
                keyPassword = keystoreProperties["keyPassword"] as String
                storeFile = file(keystoreProperties["storeFile"] as String)
                storePassword = keystoreProperties["storePassword"] as String
            }
        }
    }

    buildTypes {
        release{
            if (keystorePropertiesFile.exists() && keystoreProperties["storeFile"] != null) {
                signingConfig = signingConfigs.getByName("release")
            }
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
...

원인

 


build.gradle.kts에서 key.properties 파일 경로를 찾을 때, file() 메서드를 사용하는데 이게 android/ 폴더가 기본이라고 한다.
따라서 key.properties 파일을 이제까지 찾지 못하고 있던 거였고, 따라서 서명이 되지 않고 있는 거였다.


서명이 제대로 되는 로그는 이렇게 나온다. 로그는 println문을 따로 추가했다.


또한 key 파일이 들어가지 않고 있던걸 다음 로그를 통해 알 수 있었다. key.properties 파일을 불러오는 if문과 else에 각각 성공/실패시 출력을 추가했더니 해당 파일이 들어가지 않고 있단걸 확실히 알 수 있었다.

해결 방법

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("app/key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
    println("key.properties file found and loaded successfully.")
}else{
    println("key.properties file not found. Using default signing configuration.")
}

다음과 같이 build.gradle.kts에서 key.properties 파일 경로를 수정해줬다. key파일 출력문도 추가해준 것을 볼 수 있다.

key.properties 파일을 불러오는 if문과 else문을 추가해준것도 해당 부분이다.


appbundle이 빌드될 때 key 파일이 잘 들어가고, 문제없이 빌드되는걸 볼 수 있다.

참고로 key파일 password가 틀렸을 땐 다음과 같이 비밀번호가 틀렸다고 로그가 나온다.

해결 후 build.gradke.kts 파일

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
import java.io.FileInputStream

plugins {
    id("com.android.application")
    // START: FlutterFire Configuration
    id("com.google.gms.google-services")
    // END: FlutterFire Configuration
    id("kotlin-android")
    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
    id("dev.flutter.flutter-gradle-plugin")
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("app/key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
    println("key.properties file found and loaded successfully.")
}else{
    println("key.properties file not found. Using default signing configuration.")
}

android {
    namespace = "app_name"
    compileSdk = 36
    ndkVersion = "27.3.13750724"

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    defaultConfig {
        // AndroidManifest.xml에서 ${applicationName} 플레이스홀더를 대체
        manifestPlaceholders += mapOf("applicationName" to "io.flutter.app.FlutterApplication")
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "com.yeoseyo.byeolme"
        minSdk = 24
        targetSdk = 36
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }

    signingConfigs {
        create("release") {
            if (keystorePropertiesFile.exists() && keystoreProperties["storeFile"] != null) {
                keyAlias = keystoreProperties["keyAlias"] as String
                keyPassword = keystoreProperties["keyPassword"] as String
                storeFile = file(keystoreProperties["storeFile"] as String)
                storePassword = keystoreProperties["storePassword"] as String
            }else{
                println("WARNING: key.properties file not found or storeFile not set")
                logger.warn("key.properties file not found or storeFile not set")
            }
        }
    }

    buildTypes {
        release{
            if (keystorePropertiesFile.exists() && keystoreProperties["storeFile"] != null) {
                signingConfig = signingConfigs.getByName("release")
            }
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }


//    buildTypes {
//        release {
//            // TODO: Add your own signing config for the release build.
//            // Signing with the debug keys for now, so `flutter run --release` works.
//            signingConfig = signingConfigs.getByName("debug")
//        }
//    }
}

kotlin {
    compilerOptions {
        jvmTarget = JvmTarget.fromTarget("17")
    }
}

flutter {
    source = "../.."
}

//dependencies {
//    // Firebase BoM을 통한 버전 관리 - 모든 Firebase 라이브러리의 호환성을 보장
//    implementation(platform("com.google.firebase:firebase-bom:34.1.0"))
//
//    // Firebase Analytics - 앱 사용 통계 및 분석
//    implementation("com.google.firebase:firebase-analytics")
//
//    // Firebase Firestore - 실시간 NoSQL 데이터베이스
//    implementation("com.google.firebase:firebase-firestore")
//
//    // 추가 Firebase 서비스는 필요시 아래에 추가
//    // Firebase Auth: implementation("com.google.firebase:firebase-auth")
//    // Firebase Storage: implementation("com.google.firebase:firebase-storage")
//    // Firebase Messaging: implementation("com.google.firebase:firebase-messaging")
//}

 

결과

이렇게 해서 문제를 해결할 수 있었다.

느낀 점

  1. 해당 문제를 찾으면서 영어로 자료를 찾아도 잘 나오지 않았었다. ai의 발전으로 stackoverflow 및 기타 커뮤니티에 글이 올라오지 않게 된 부작용이다.
  2. 다만 ai을 이용해서 문제의 원인을 추론하고, 해결할 수 있었다. gemini가 file()이 android/가 기본 폴더라는걸 알려주지 않았더라면 많이 어려웠을 것이다.
  3. 나중에 발견했는데 flutter 공식 홈페이지에선 key.properties 파일을 android 경로에 올리라고 나와 있었다... 항상 공식 문서를 잘 읽어보자.

4. build appbundle

728x90
반응형
LIST
728x90
반응형

https://gemini.google.com/share/bf92d0cb5040 참고

1. 시작하며: 왜 하필 Xubuntu였을까요?

안녕하세요! 저는 현재 메인 OS로 Windows 11을 사용하고 있는 개발자입니다. 물론 윈도우의 WSL2 기능도 훌륭하지만, OS 자체가 기본적으로 점유하는 메모리와 백그라운드 프로세스가 상당히 많다는 점은 늘 아쉬움으로 남았습니다. 특히 안드로이드 에뮬레이터와 브라우저를 동시에 띄우면 팬 소리가 거슬릴 정도로 커지곤 했죠.

저에게 필요한 건 화려한 애니메이션이나 투명 효과 같은 UI가 아니었습니다. **오직 내 코드가 0.1초라도 빨리 빌드되고, 에뮬레이터가 부드럽게 돌아가는 '쾌적한 성능'**이었습니다.

그래서 다음과 같은 이유로 새로운 환경을 구축하게 되었습니다.

  1. Native Linux: 가상화의 오버헤드 없이 하드웨어 성능을 100% 온전히 사용하고 싶었습니다.
  2. Xubuntu (Xfce): 예쁘지만 무거운 표준 우분투(GNOME) 대신, UI는 조금 투박해도 RAM을 적게 차지하는 가벼운 Xubuntu를 선택했습니다.
  3. 목표: 윈도우의 방해 없는 쾌적한 Flutter 개발 환경 구축입니다.

2. 준비하기: 안전한 설치를 위한 준비물

설치 전 가장 중요한 것은 '준비'입니다. 혹시 모를 상황에 대비해 윈도우의 중요 데이터는 꼭 백업해 주시길 바랍니다.

🛠 준비물

  • USB 드라이브: 8GB 이상 (내부 데이터는 삭제되니 비어있는 것을 준비해주세요)
  • Rufus: 부팅 USB를 만들기 위한 프로그램
  • Xubuntu 25.10 LTS ISO: 공식 홈페이지에서 다운로드

Step 1: 윈도우에서 공간 확보하기

먼저 리눅스가 설치될 공간을 마련해 주어야 합니다.

  1. Win + X 키를 누르고 **'디스크 관리'**를 실행합니다.
  2. 가장 용량이 큰(C:) 드라이브를 우클릭하여 **'볼륨 축소'**를 선택합니다.
  3. 저는 개발용으로 넉넉하게 100GB (약 102400MB) 정도를 할당했습니다.
  4. 축소 후 검은색으로 표시된 '할당되지 않음(Unallocated)' 공간이 생겼는지 확인하고 창을 닫습니다.

Step 2: 부팅 USB 제작

다운로드한 Xubuntu ISO 파일을 Rufus 프로그램을 이용해 USB에 굽습니다.

  • 파티션 방식: GPT
  • 대상 시스템: UEFI (윈도우 11은 대부분 이 환경입니다)

시작을 누르면 usb에 iso파일이 설치됩니다. (10분 정도 소요)

이때 usb 안의 모든 정보는 지워지니 사전에 usb가 비어있는지 확인하고 진행하는 것이 좋습니다.


3. 실행하기: 파티션 나누기 (가장 중요한 단계!)

Step 1: BIOS 설정 및 부팅

USB를 꽂고 재부팅하면서 BIOS 진입 키(F2 또는 Del)를 연타하여 설정 화면으로 들어갑니다.

  • Secure Boot: 호환성 문제를 피하기 위해 **Disabled(끄기)**로 설정하는 것을 추천합니다.
  • Boot Priority (부팅 순서): USB를 1순위로 올려줍니다.

Step 2: 설치 및 파티션 수동 설정

설치 화면에 진입하면 여러 설정 단계가 나오는데요, '설치 형식' 단계에서 집중해주셔야 합니다. 저는 파티션을 명확하게 관리하기 위해 **'기타(Something else)'**를 선택했습니다.

아까 윈도우에서 만들어둔 **'남은 공간(free space)'**을 선택하여 다음과 같이 두 개의 파티션으로 나누었습니다.

1. Swap 파티션 (가상 메모리)

  • RAM이 부족할 때를 대비한 공간입니다.
  • 크기: 8GB (제 컴퓨터의 물리 RAM 크기와 비슷하게 맞췄습니다)
  • 용도: swap area (스왑 영역)

2. Root 파티션 (/)

  • 실질적으로 리눅스와 프로그램이 설치될 메인 공간입니다.
  • 크기: 남은 공간 전체
  • 용도: Ext4 journaling file system
  • 마운트 위치: / (슬래시 하나)

설정을 마치고 설치를 진행하니 약 10~15분 정도 소요되었습니다.


4. 환경 설정: Flutter 개발 준비 완료

설치가 끝나고 부팅을 하니, 투박하지만 굉장히 빠른 반응 속도의 Xfce 화면이 반겨줍니다. 바로 터미널(Ctrl+Alt+T)을 열고 개발 환경을 세팅했습니다.

1. 시스템 업데이트 & 필수 도구 설치

가장 먼저 패키지들을 최신 상태로 업데이트해주었습니다.

sudo apt update && sudo apt upgrade -y
sudo apt install git curl unzip build-essential -y

2. 한글 입력기 설정 (IBus)

설정 메뉴의 'Language Support'에서 한국어 패키지를 설치하고, 키보드 입력 시스템을 IBus로 확인했습니다. 이후 설정에서 Korean (Hangul)을 추가하니 한글 입력도 아주 잘 됩니다.

단, 한국어-hangul으로 설정되어야 한글/영어가 잘 바뀝니다. 한국어-korean 언어팩은 삭제했습니다.

 

또한, 저는 스크롤 방향이 윈도우가 익숙해서 스크롤 방향을 반대로 바꿔줬습니다. 설정-마우스와 터치패드-장치 선택 후, 스크롤 방향 반전을 눌러주면 됩니다.

3. Flutter & VS Code 설치

우분투 계열은 Snap을 이용하면 복잡한 설정 없이 한 번에 설치할 수 있어 정말 편리합니다.

# VS Code 설치
sudo snap install code --classic

# Flutter SDK 설치
sudo snap install flutter --classic

# 리눅스 데스크톱 앱 빌드 라이브러리 (선택 사항)
sudo apt install clang cmake ninja-build pkg-config libgtk-3-dev -y

5. 결과: 가벼움, 그 이상의 쾌적함

모든 세팅을 마치고 재부팅을 해보았습니다. 윈도우와 Xubuntu 중 선택할 수 있는 화면(GRUB)이 뜨네요. Xubuntu로 진입하여 시스템 모니터를 확인해 보았습니다.

📊 리소스 점유율 비교 (아무것도 안 켠 상태)

구분 Windows 11 Xubuntu (Xfce)
RAM 점유량 3.8 GB 500 MB
백그라운드 프로세스 100개 이상 30개 미만

🚀 직접 체감한 성능

  1. 기본적으로 flutter에서 초기 빌드시에 cpu와 메모리를 정말 많이 잡아먹는데, 빌드시에도 메모리 사용량이 절반을 넘어가지 않는 모습을 볼 수 있습니다.
  2. 또한 빌드 시간, 속도 모두 윈도우 환경보다 빠릅니다.

다만

  1. vscode을 킬 때마다 keytools을 사용할 수 없다는 에러가 있고
  2. 안드로이드 스튜디오에서 에뮬레이터를 생성해도 vscode에선 보이지 않는 문제가 있습니다. 아마 android studio 설치때 일부 라이브러리를 찾을 수 없다고 나와 공식 명령어 대신 다른 명령어를 사용했는데 이 때문인 것 같습니다.

💡 마무리하며

디자인이 조금 옛날 느낌이면 어떤가요? 테마는 나중에 원하면 언제든 예쁘게 꾸밀 수 있으니까요.
무엇보다 한정된 하드웨어 자원을 OS가 낭비하지 않고, 오로지 제가 작성하는 코드와 개발 도구에만 집중시킬 수 있다는 점이 정말 만족스럽습니다.

혹시 윈도우에서 개발 환경이 무겁게 느껴져서 고민이신 분들, 특히 Flutter나 모바일 앱 개발자분이시라면 가벼운 리눅스(Xubuntu) 듀얼 부팅 구성을 조심스럽게 추천해 드립니다.


 

728x90
반응형
LIST

+ Recent posts