Skip to content

개발 구조 이해하기

앱인토스는 개발 편의를 위해 SDK·API를 제공해요.
SDK는 WebViewReact Native, 두 가지 방식으로 제공돼요.

이 문서에서는 앱인토스의 전체 개발 구조와 주요 설정 방법, 그리고 개발 시 반드시 알아야 할 유의사항을 함께 안내해요.

⚠️ 앱인토스 API를 사용하려면 mTLS 기반의 서버 간(Server-to-Server) 통신이 반드시 필요해요.
mTLS 인증서는 파트너사 서버와 앱인토스 서버 간 통신을 암호화하고 쌍방 신원 확인에 사용돼요.

다음 기능을 사용하려면 반드시 mTLS 인증서를 통한 통신이 필요해요.

  • 토스 로그인
  • 토스 페이
  • 인앱 결제
  • 기능성 푸시, 알림
  • 프로모션(토스 포인트)

mTLS 인증서 발급 방법

서버 mTLS 인증서는 콘솔에서 직접 발급할 수 있어요.

1. 앱 선택하기

앱인토스 콘솔에 접속해 인증서를 발급받을 앱을 선택하고, 왼쪽 메뉴에서 mTLS 인증서 탭을 클릭하세요.
+ 발급받기 버튼을 누르면 돼요.

2. 인증서 발급·보관

mTLS 인증서가 발급되면 인증서와 키 파일을 다운로드할 수 있어요.

참고하세요

  • 인증서와 키 파일은 유출되지 않도록 안전한 위치에 보관하세요.
  • 인증서가 만료되기 전에 반드시 재발급해 주세요.

발급된 인증서는 콘솔에서 아래와 같이 확인할 수 있어요.

일반적으로 인증서는 하나만 사용하지만, 무중단 교체를 위해 두 개 이상 등록해 둘 수도 있어요.
그래서 콘솔에서는 인증서를 여러 개 등록/관리할 수 있게 구성했어요.

인증서로 요청 보내기 예제

앱인토스 서버에 요청하려면, 발급받은 인증서/키서버 애플리케이션에 설정하세요.
아래는 다양한 언어에서 mTLS를 사용해 요청을 보내는 예제예요.
(환경에 맞게 경로·알고리즘·TLS 버전 등을 조정하세요.)

Kotlin 예제
kotlin
import java.security.KeyStore
import java.security.cert.X509Certificate
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.io.FileReader
import java.io.ByteArrayInputStream
import java.util.Base64
import javax.net.ssl.*

class TLSClient {
    fun createSSLContext(certPath: String, keyPath: String): SSLContext {
        val cert = loadCertificate(certPath)
        val key = loadPrivateKey(keyPath)

        val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
        keyStore.load(null, null)
        keyStore.setCertificateEntry("client-cert", cert)
        keyStore.setKeyEntry("client-key", key, "".toCharArray(), arrayOf(cert))

        val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
        kmf.init(keyStore, "".toCharArray())

        return SSLContext.getInstance("TLS").apply {
            init(kmf.keyManagers, null, null)
        }
    }

    private fun loadCertificate(path: String): X509Certificate {
        val content = FileReader(path).readText()
            .replace("-----BEGIN CERTIFICATE-----", "")
            .replace("-----END CERTIFICATE-----", "")
            .replace("\\s".toRegex(), "")
        val bytes = Base64.getDecoder().decode(content)
        return CertificateFactory.getInstance("X.509")
            .generateCertificate(ByteArrayInputStream(bytes)) as X509Certificate
    }

    private fun loadPrivateKey(path: String): java.security.PrivateKey {
        val content = FileReader(path).readText()
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace("\\s".toRegex(), "")
        val bytes = Base64.getDecoder().decode(content)
        val spec = PKCS8EncodedKeySpec(bytes)
        return KeyFactory.getInstance("RSA").generatePrivate(spec)
    }

    fun makeRequest(url: String, context: SSLContext): String {
        val connection = (URL(url).openConnection() as HttpsURLConnection).apply {
            sslSocketFactory = context.socketFactory
            requestMethod = "GET"
            connectTimeout = 5000
            readTimeout = 5000
        }

        return connection.inputStream.bufferedReader().use { it.readText() }.also {
            connection.disconnect()
        }
    }
}

fun main() {
    val client = TLSClient()
    val context = client.createSSLContext("/path/to/client-cert.pem", "/path/to/client-key.pem")
    val response = client.makeRequest("https://apps-in-toss-api.toss.im/endpoint", context)
    println(response)
}
Python 예제
python
import requests

class TLSClient:
    def __init__(self, cert_path, key_path):
        self.cert_path = cert_path
        self.key_path = key_path

    def make_request(self, url):
        response = requests.get(
            url,
            cert=(self.cert_path, self.key_path),
            headers={'Content-Type': 'application/json'}
        )
        return response.text

if __name__ == '__main__':
    client = TLSClient(
        cert_path='/path/to/client-cert.pem',
        key_path='/path/to/client-key.pem'
    )
    result = client.make_request('https://apps-in-toss-api.toss.im/endpoint')
    print(result)
JavaScript(Node.js) 예제
js
const https = require('https');
const fs = require('fs');

const options = {
  cert: fs.readFileSync('/path/to/client-cert.pem'),
  key: fs.readFileSync('/path/to/client-key.pem'),
  rejectUnauthorized: true,
};

const req = https.request(
  'https://apps-in-toss-api.toss.im/endpoint',
  { method: 'GET', ...options },
  (res) => {
    let data = '';
    res.on('data', (chunk) => (data += chunk));
    res.on('end', () => {
      console.log('Response:', data);
    });
  }
);

req.on('error', (e) => console.error(e));
req.end();
C# 예제
c#
using System;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(
            new X509Certificate2("/path/to/client-cert.pem")
        );

        using var client = new HttpClient(handler);
        var response = await client.GetAsync("https://apps-in-toss-api.toss.im/endpoint");
        string body = await response.Content.ReadAsStringAsync();
        Console.WriteLine(body);
    }
}
C++ 예제(libcurl 사용)
cpp
#include <curl/curl.h>
#include <iostream>
#include <string>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
    userp->append((char*)contents, size * nmemb);
    return size * nmemb;
}

int main() {
    CURL* curl = curl_easy_init();
    if (curl) {
        std::string response;
        curl_easy_setopt(curl, CURLOPT_URL, "https://apps-in-toss-api.toss.im/endpoint");
        curl_easy_setopt(curl, CURLOPT_SSLCERT, "/path/to/client-cert.pem");
        curl_easy_setopt(curl, CURLOPT_SSLKEY, "/path/to/client-key.pem");
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

        CURLcode res = curl_easy_perform(curl);
        if (res == CURLE_OK) {
            std::cout << "Response: " << response << std::endl;
        } else {
            std::cerr << "Error: " << curl_easy_strerror(res) << std::endl;
        }

        curl_easy_cleanup(curl);
    }

    return 0;
}

개발 시 유의사항

앱인토스 환경에서 개발할 때 알아두면 좋은 주의사항이에요.

CORS 허용을 위한 Origin 등록

다음 도메인을 Origin 허용 목록에 등록해 주세요

  • https://<appName>.apps.tossmini.com : 실제 서비스 환경
  • https://<appName>.private-apps.tossmini.com : 콘솔 QR 테스트 환경

iOS의 서드파티 쿠키 차단 정책

iOS/iPadOS 13.4 이상에서는 서드파티 쿠키가 완전히 차단돼요.
앱인토스 도메인이 아닌 파트너사 도메인에서 쿠키 기반 로그인을 구현하면 정상 동작하지 않아요.

App Transport Security (ATS)

샌드박스 환경에서는 HTTP 통신이 가능하지만, 서비스 환경에서는 HTTPS만 허용돼요.
HTTP 요청은 샌드박스 앱에서만 정상적으로 동작한다는 점을 유의하세요.

게임 리소스 분리 다운로드 권장

앱인토스 미니앱의 빌드 파일은 압축 해제 기준 100MB 이하만 허용돼요.
빌드 파일에 모든 리소스(이미지, 사운드, 영상 등)를 포함하면 용량을 초과할 수 있어요.
필수적으로 리소스 다운로드를 빌드 파일과 분리해야 합니다.

  • 앱 실행에 꼭 필요한 최소 리소스만 빌드 파일에 포함하세요.
  • 대용량 리소스는 외부 스토리지/CDN을 통해 다운로드하도록 구성해 주세요.
  • 추가 리소스는 단계적 다운로드(Lazy Loading)를 적용하면 사용자 경험이 향상돼요.

외부 3자 로깅 솔루션 제한

정책상 외부 3자 로깅 솔루션 사용은 제한돼요. 현재 허용되는 솔루션은 아래와 같아요.
추가 제안이 있으면 채널톡으로 보내주세요. 내부 검토 후 안내드릴게요.

[ 시스템 로거 ]

  • Sentry

[ 분석용 로거 ]

  • Google Analytics
  • Unity Analytics
  • Amplitudes (단, Webview 에서만 사용 가능)

비게임 미니앱은 '피처' 단위로 노출돼요

미니앱 개발이 완료되면, 토스앱 내에서 피처(기능) 단위로 사용자에게 노출할 수 있어요.
하나의 서비스(앱)는 반드시 하나 이상의 피처를 가져야 하며, 최대 3개까지 등록할 수 있어요.

피처란?

토스 사용자에게 노출되는 기능 단위의 랜딩 페이지예요.

예:

  • 오늘의 운동 루틴 보기 → Page A로 이동
  • 식단 기록하기 → Page B로 이동
  • 건강 리포트 확인하기 → Page C로 이동

피처 등록 위치

  • 최초 등록: 콘솔 > 앱 출시 > 검토 요청하기 단계
  • 이후 관리: 콘솔 > 앱 내 기능 탭에서 수정/추가

등록 시 피처 이름이동할 주소를 꼭 입력해주세요.

  • 기본 스킴: intoss://{appName} (하위 경로: intoss://{appName}/path)
  • 쿼리 파라미터 설정 가능
  • 제공 기능 단위로 세분화하여 등록
  • 최대 3개까지 등록 가능
  • 기능 이름은 ~기록하기, ~보러 가기‘~하기’ 형태 권장(명사형도 가능)

개발 환경별 피처 구성

1. Webview

라우터 경로를 피처 주소와 매핑해요.

tsx
<Route path="/search" element={<SearchPage />} />

피처 주소는 intoss://{appName}/search로 입력하면 해당 페이지로 이동할 수 있어요.

2. React Native (Bedrock 기반)

Next.js 유사 파일 기반 라우팅을 사용해요.
/pages/search.tsx/search 경로 매핑 → intoss://{appName}/search로 진입 시 렌더링
자세한 구조는 파일 기반 라우팅 이해하기 를 참고하세요.

자주 묻는 질문

mTLS 미적용 상태에서 API를 호출하면 발생해요.

간편 로그인·결제·광고 등 API 기능을 쓰기 전에 서버에 mTLS를 설정한 뒤 호출해주세요.

인증서/키는 서버 인증에 직접 사용돼요.

노출될 경우 타 파트너가 인증서를 도용해 API를 호출하거나, 의도치 않은 포인트 지급 등 사고가 날 수 있어요.

즉시 해당 인증서를 폐기(삭제) 후 재발급하세요.

mTLS 인증서는 390일 동안 유효해요.

만료되면 인증이 실패하고 서버 간 통신이 중단돼요.

콘솔에서 미리 새 인증서를 발급받고 교체해 주세요.

추후에는 인증서 만료 1개월 전, 1주일 전, 1일 전에 이메일로 안내할 예정이에요.

제한 없이 여러 개 발급 가능해요.

대부분 한개를 쓰지만, 무중단 교체를 위해 2개 이상을 병행 등록하기도 해요.

이를 위해 콘솔에서 다중 인증서 관리를 지원해요.

API 호출 시 CORS 정책에 의해 차단되는 경우가 있어요.

아래 도메인을 반드시 허용해야 정상적으로 통신돼요.

  • https://.apps.tossmini.com
  • https://.private-apps.tossmini.com

라이브 환경에서는 HTTPS만 허용돼요.

샌드박스에서는 http 통신이 가능하지만, 실제 서비스에서는 https 프로토콜을 사용해야 해요.

iOS/iPadOS 13.4 이상에서는 서드파티 쿠키가 완전히 차단돼요.

앱인토스 도메인이 아닌 곳에서 쿠키를 이용한 로그인은 동작하지 않아요.

토큰 기반 인증 또는 OAuth 방식으로 전환해 주세요.

앱인토스 미니앱은 빌드 파일 크기가 압축 해제 기준 100MB 이하로 제한돼요.

이미지·사운드·영상 등 대용량 리소스는 외부 스토리지나 CDN을 통해 다운로드하도록 분리하세요.

추가 리소스는 단계적 로딩(Lazy Loading)을 적용하면 사용자 경험도 좋아져요.