이 글의 핵심

구독 주소는 설정을 가져오는 경로이고, Base64는 텍스트 인코딩, 공유 링크는 노드 정보를 담으며, 네이티브 JSON은 코어 전체의 실행 설정을 담을 수 있습니다. 구독을 열 수는 있지만 가져오기에 실패하거나 노드를 옮기려는 경우, 먼저 응답 내용을 확인한 뒤 로컬에서 디코딩하고 프로토콜 필드를 점검하세요. 변환으로 노드 매개변수는 옮길 수 있지만 DNS, 라우팅, 업데이트 관계까지 그대로 유지된다고 볼 수는 없습니다.

구독 주소, 응답 본문, 노드 설정 구분하기

https://sub.example.com/list?token=demo 같은 주소는 클라이언트가 콘텐츠를 요청하는 데 사용됩니다. URL만으로 반환 형식을 판단할 수는 없습니다. 이 글의 도메인, UUID, 매개변수는 구조를 보여 주기 위한 예시이며 실제 연결에 사용할 수 없습니다. 실제 구독 주소의 토큰에는 보통 접근 권한이 있으므로 주소를 공유하면 노드 전체가 노출될 수 있습니다.

기존 V2Ray 구독은 여러 공유 링크를 줄바꿈으로 연결한 뒤 전체 텍스트를 Base64로 인코딩하는 경우가 많습니다. 인코딩하지 않은 링크 목록을 바로 반환하거나, 클라이언트 요청에 따라 서로 다른 내용을 반환하는 서비스도 있습니다. 따라서 브라우저에서 확인한 결과와 클라이언트가 업데이트할 때 받는 결과가 다를 수 있습니다.

구독 주소와 링크 목록

권장

노드를 꾸준히 관리하려면 기존 구독 주소를 유지하는 편이 좋습니다. 서버에서 목록을 업데이트하고 클라이언트에서 그룹별로 가져올 수 있으며, 본문은 Base64일 수도 있고 일반 텍스트 링크일 수도 있습니다.

적합한 경우: 노드를 계속 업데이트하거나 데스크톱과 Android에 각각 가져오려는 경우

단일 노드 공유 링크

흔히 vmess:// 또는 vless://로 시작합니다. 링크에는 노드 연결 매개변수가 담기며, 전체 실행 설정을 뜻하지는 않습니다.

적합한 경우: 노드 하나를 임시로 전달하거나 특정 항목의 필드를 확인할 때

코어 네이티브 JSON

인바운드, 아웃바운드, DNS, 라우팅을 포함할 수 있습니다. 파일의 실행 가능 여부는 확장자가 JSON인지가 아니라 대상 코어와 설정 구조에 따라 달라집니다.

적합한 경우: 인바운드, 아웃바운드, 라우팅을 직접 설정해야 하는 구성 관리 담당자

v2rayN과 v2rayNG에서 가져오는 방법

두 클라이언트 모두 일반적인 VMess·VLESS 공유 링크와 기존 링크 구독을 관리할 수 있습니다. 다만 텍스트를 인식한다고 해서 대상 코어가 모든 매개변수를 지원하는 것은 아닙니다. 버전에 따라 메뉴 이름이 달라질 수 있으므로, 아래에서는 기능별로 구분했습니다. 구독 URL을 단일 노드 입력란에 붙여 넣지 마세요.

입력 형식별 가져오기 방법
입력 형식v2rayN 데스크톱v2rayNG Android
HTTPS 구독 주소「구독 그룹」→「구독 그룹 설정」에서 그룹을 만들고 주소를 입력한 다음 해당 그룹을 업데이트합니다「구독 그룹 설정」에서 주소를 추가한 뒤 목록으로 돌아가 「구독 업데이트」를 실행합니다
일반 텍스트 공유 링크전체 링크를 복사한 뒤 「클립보드에서 URL 일괄 가져오기」와 같은 메뉴를 사용합니다오른쪽 위 「+」→「클립보드에서 가져오기」
Base64 텍스트 전체먼저 구독 업데이트 기능으로 파싱해 보세요. 수동으로 옮길 때는 디코딩한 후 링크 목록을 가져옵니다구독 그룹을 사용하는 것이 좋습니다. 수동으로 가져와도 인식되지 않으면 먼저 디코딩해 공유 링크로 변환합니다
코어 네이티브 JSON사용자 지정 구성 메뉴에서 대상 코어에 맞는 설정을 선택합니다. 일반 구독 주소처럼 사용할 수 없습니다사용자 지정 구성 가져오기 메뉴를 사용합니다. 지원 범위는 사용 중인 Xray 코어와 클라이언트 버전에 따라 달라집니다

v2rayNG는 Xray 코어를 사용하고, v2flyNG는 V2Fly 코어를 사용합니다. JSON 문법이 올바르더라도 두 코어가 해당 보안 기능이나 설정 필드를 모두 지원한다고 볼 수는 없습니다. 특히 VLESS, REALITY 조합을 사용할 때는 형식을 변환하기 전에 대상 코어의 지원 여부를 확인하세요. 형식 변환으로 프로토콜 지원이 추가되지는 않습니다.

Base64 구독 디코딩: 먼저 바깥 형식을 확인하고 각 행을 살펴보세요

먼저 오류 페이지가 아니라 응답 본문을 받았는지 확인하세요. 본문이 <!DOCTYPE html>, 로그인 안내 또는 API 오류로 시작한다면 디코딩할 구독 콘텐츠가 아닙니다. 파일 확장자만 바꾸거나 반복해서 가져와도 인증 만료나 서버 오류는 해결되지 않습니다.

  1. 응답 본문을 로컬 텍스트 파일로 저장하고 원본은 보관하세요. 복사 과정에서 덧붙은 설명 문구는 제거하고 URL을 Base64 본문으로 취급하지 마세요.
  2. 본문이 이미 vmess:// 또는 vless://로 시작한다면 일반 텍스트 링크 목록으로 처리하고 본문 전체를 다시 디코딩하지 마세요.
  3. 본문이 긴 인코딩 문자열이라면 바깥쪽 Base64를 한 번 디코딩한 뒤 줄바꿈으로 나누고 빈 줄을 제거하세요.
  4. 각 행의 프로토콜 접두사를 확인하세요. 어떤 행이 vmess://로 시작하면 뒤에 VMess 공유 정보 인코딩이 한 겹 더 있을 수 있습니다. 그렇다고 구독 전체를 계속 반복해서 디코딩해야 하는 것은 아닙니다.

아래 코드는 구독 주소를 요청하지 않습니다. 이미 받은 텍스트를 로컬의 빈 페이지에서 개발자 도구 콘솔로 처리할 때만 사용하세요. 먼저 함수를 정의한 뒤 decodeSubscription(prompt("구독 응답 본문 붙여넣기"))를 실행합니다. 신뢰할 수 없는 웹페이지의 콘솔에서 실제 인증 정보를 처리하지 마세요.

function decodeSubscription(input) {
  const raw = String(input ?? "").trim();
  if (!raw) throw new Error("본문이 비어 있습니다");
  if (/^(vmess|vless):\/\//i.test(raw)) return raw;

  let encoded = raw.replace(/\s/g, "")
    .replace(/-/g, "+").replace(/_/g, "/");
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
    throw new Error("Base64 문자 규칙에 맞지 않는 콘텐츠입니다");
  }
  encoded = encoded.replace(/=+$/, "");
  if (encoded.length % 4 === 1) {
    throw new Error("인코딩된 길이가 비정상입니다. 전체 내용을 복사했는지 확인하세요");
  }
  encoded += "=".repeat((4 - encoded.length % 4) % 4);
  const bytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
}

공유 링크를 Base64로 만들기: 줄 단위로 연결한 뒤 인코딩

일반 텍스트 목록을 기존 Base64 구독 본문으로 만들려면 링크를 한 줄에 하나씩 배치하고 UTF-8 바이트로 변환한 다음 Base64로 인코딩하세요. 이렇게 만들어지는 것은 정적인 본문이며, 업데이트 가능한 구독 서비스가 자동으로 생성되는 것은 아닙니다. 결과를 로컬 파일로 저장해도 클라이언트가 접근할 수 있는 구독 URL이 생기는 것은 아닙니다.

const link = "vless://[email protected]:443?encryption=none&security=tls&type=tcp&sni=node.example.com#Example-TLS";

function encodeSubscription(links) {
  const text = links.map(item => item.trim()).filter(Boolean).join("\n");
  const bytes = new TextEncoder().encode(text);
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary);
}

const encoded = encodeSubscription([link]);
console.log(encoded);
console.log(decodeSubscription(encoded) === link);

중국어 메모도 인코딩 대상에 포함되므로 중국어가 들어간 문자열에 btoa를 바로 사용하지 마세요. 먼저 TextEncoder로 UTF-8 바이트로 변환하면 문자 범위 오류를 방지할 수 있습니다. 마지막 줄의 왕복 비교 결과는 true여야 합니다. 이는 예시 텍스트가 인코딩 과정에서 바뀌지 않았음을 확인할 뿐, 노드 연결 가능 여부를 검증하지는 않습니다.

443
예시 원격 TLS 포트
10808
이후 예시에서 가정하는 로컬 SOCKS 포트
3 → 4
Base64는 3바이트를 4문자로 인코딩합니다

위 숫자는 프로토콜 인코딩 규칙과 설정 예시일 뿐, 속도 측정 결과나 모든 클라이언트 버전의 기본값이 아닙니다. Base64는 압축이 아닙니다. 끝의 패딩을 제외하면 인코딩된 길이는 원래 바이트 수의 약 4/3, 즉 약 3분의 1 늘어납니다.

공유 링크와 네이티브 JSON: 필드 매핑만으로 전체 설정을 옮길 수는 없습니다

앞서 살펴본 VLESS 링크를 예로 들면 호스트와 포트는 아웃바운드 서버 설정으로, UUID는 사용자 설정으로 들어가며, security=tls는 TLS, sni는 TLS 서버 이름에 해당합니다. 아래는 Xray 형식의 단일 아웃바운드 객체이며, 그대로 실행할 수 있는 완전한 설정 파일이 아닙니다. 전체 설정의 outbounds 배열에 넣어야 합니다.

{
  "tag": "proxy",
  "protocol": "vless",
  "settings": {
    "vnext": [
      {
        "address": "node.example.com",
        "port": 443,
        "users": [
          {
            "id": "11111111-1111-4111-8111-111111111111",
            "encryption": "none"
          }
        ]
      }
    ]
  },
  "streamSettings": {
    "network": "tcp",
    "security": "tls",
    "tlsSettings": {
      "serverName": "node.example.com"
    }
  }
}

실행 가능한 설정을 만들려면 로컬 인바운드, 아웃바운드 선택, 필요한 라우팅 설정도 지정해야 합니다. 예를 들어 로컬 SOCKS가 127.0.0.1:10808에서 수신하도록 설정했다면 이를 사용하는 애플리케이션에도 같은 주소와 포트를 입력해야 합니다. 원격 포트 443을 로컬 프록시 포트로 설정하면 안 됩니다. 두 포트는 연결 경로의 서로 다른 끝에 있습니다.

변환할 때 항목별로 확인할 필드
공유 링크의 내용네이티브 설정에서 대응하는 위치자주 놓치는 점
호스트, 포트, UUIDsettings.vnext의 서버 및 사용자포트는 숫자여야 하며 UUID 앞뒤에 공백이 없어야 합니다
type, 전송 매개변수streamSettingsWebSocket 경로와 Host는 둘 다 옮겨야 합니다
security、sniTLS 또는 해당 보안 설정서버 주소와 SNI는 서로 다를 수 있으므로 임의로 같게 설정하지 마세요
flow 및 REALITY 매개변수사용자 필드와 해당 보안 설정서버에서 요구하는 flow, 공개 키, short ID 등의 정보를 유지해야 합니다
# 뒤의 메모클라이언트 표시 이름 또는 직접 관리하는 태그메모는 인증 정보가 아니며 라우팅 규칙으로 자동 변환되지 않습니다

VMess 공유 링크는 보통 vmess:// 뒤에 공유 객체를 Base64로 인코딩해 붙이는 형식입니다. 객체의 add, port, id, net, path 등의 필드는 공유 형식에 따른 것이며 코어 네이티브 설정의 최상위 필드가 아닙니다. 디코딩한 객체를 실행 설정으로 바로 저장하면 구조가 맞지 않아 실패하는 경우가 많습니다.

두 가지 이전 방법: 노드 매개변수와 전체 설정을 구분하세요

노드만 이전
  • 먼저 클라이언트에서 공유 링크를 내보낸 다음 대상 클라이언트로 가져오세요.
  • UUID, 전송 방식, TLS, SNI, 경로 등의 연결 필드를 확인하세요.
  • 대상 클라이언트에서 시스템 프록시, DNS, 라우팅을 다시 설정하세요.
전체 실행 설정 유지
  • JSON에 포함된 인바운드, 아웃바운드 태그와 라우팅 참조 관계를 유지하세요.
  • 대상 코어가 모든 필드를 지원하는지 확인하고 로컬 수신 포트가 충돌하는지도 점검하세요.
  • 사용자 지정 구성 메뉴로 가져오고 단일 노드 링크로 억지로 축약하지 마세요.

네이티브 JSON을 공유 링크로 되돌릴 때는 보통 일부 아웃바운드만 추출할 수 있습니다. 여러 아웃바운드 경로, DNS, 라우팅 규칙을 하나의 링크로 손실 없이 표현하는 범용 방법은 없습니다.

변환 후 확인: 업데이트 성공과 연결 성공은 따로 확인하세요

구독 업데이트 성공은 클라이언트가 콘텐츠 일부 또는 전체를 가져와 파싱했다는 뜻일 뿐, 모든 노드를 사용할 수 있다는 의미는 아닙니다. 기존 구독 그룹을 유지하고 변환 결과는 별도의 테스트 그룹에 가져오는 것이 좋습니다. 노드 하나를 먼저 확인한 뒤 나머지를 처리하세요. 수동으로 내보낸 정적 링크에는 원래 구독의 업데이트 관계도 이어지지 않습니다.

  1. 항목 확인: 변환 전후의 노드 수를 비교하고 빈 줄, 중복 항목, 지원되지 않아 건너뛴 프로토콜이 있는지 살펴보세요.
  2. 필드 확인: 노드 편집 화면에서 주소, 포트, UUID, 전송 방식, 경로, SNI, 보안 설정을 확인하세요. 메모 이름이 같은지만 보지 마세요.
  3. 실행 확인: 테스트 노드를 선택하고 코어 로그를 확인하세요. 설정 파싱 오류, 인증 실패, 연결 시간 초과는 각각 구분해 해결해야 합니다.
  4. 앱 연결 확인: 시스템 프록시 또는 앱 프록시가 실제 로컬 수신 포트를 가리키는지 확인한 다음, 라우팅이 테스트 요청을 예상한 아웃바운드로 보내는지 점검하세요.

디코딩했는데도 긴 문자열이 남습니다. 한 번 더 디코딩해야 하나요?

먼저 vmess://가 나타났는지 확인하세요. 있다면 해당 행의 뒷부분만 VMess 공유 형식에 따라 처리하고 목록 전체를 계속 디코딩하지 마세요. 프로토콜 접두사도 없고 알아볼 수 있는 JSON 구조도 없다면 원래 응답이 완전한지, 서버가 실제로 어떤 형식으로 반환하는지 먼저 확인하세요.

JSON 파일은 열리는데 클라이언트에서 가져오기에 실패하는 이유는 무엇인가요?

최상위 필드를 확인하세요. add, id 등의 필드만 있는 객체는 VMess 공유 정보일 수 있습니다. inbounds, outbounds를 포함한 객체라면 코어 실행 설정일 가능성이 있습니다. 후자는 사용자 지정 구성 메뉴로 가져온 뒤 대상 코어가 해당 필드를 지원하는지 확인해야 합니다.

같은 구독이 데스크톱에서는 업데이트되는데 Android에서는 비어 있는 이유는 무엇인가요?

먼저 양쪽의 구독 주소가 완전한지 확인한 뒤, 클라이언트 요청에 따라 응답 내용이 달라지는지 살펴보세요. 응답에 문제가 없다면 프로토콜과 전송 방식의 지원 여부를 비교하세요. 명확한 링크 하나를 「+」→「클립보드에서 가져오기」로 따로 테스트하면 구독 전체를 반복해서 변환하지 않고도 문제가 있는 항목을 찾을 수 있습니다.

JSON 전체를 VLESS 링크 하나로 변환할 수 있나요?

대상 설정이 링크로 표현 가능한 VLESS 아웃바운드 하나일 때만 해당 필드를 추출해 링크를 만드는 것이 적절합니다. 먼저 protocol이 vless인 아웃바운드를 찾아 연결 매개변수를 유지하세요. 인바운드, DNS, 라우팅, 다른 아웃바운드는 별도로 보관해야 하며 한 번의 변환으로 모두 유지된다고 보장할 수 없습니다.