TechFeedTechFeed
Security

2차 인증 직접 구현하기 — 인증 앱·문자·TOTP 비교와 슈퍼베이스 코드

1인 서비스에 2차 인증을 붙이는 가장 현실적인 길을 코드와 함께 정리합니다. 인증 앱 기반 TOTP와 SMS 문자, 보안 키를 비용과 보안으로 비교하고, 슈퍼베이스 MFA의 enroll·challenge·verify 흐름과 speakeasy 직접 구현, 복구 코드 발급, 카카오·네이버에 익숙한 한국 사용자를 위한 화면 설계까지 다룹니다. 2026년 6월 기준 공식 문서로 검증했습니다.

by

한 줄 요약: 1인 서비스에 2차 인증(2FA)을 붙이는 가장 현실적인 방법은 TOTP(시간 기반 일회용 비밀번호)입니다. SMS는 비용과 보안 양쪽에서 불리하고, 인증 앱(Google Authenticator·구글 OTP 등)을 쓰는 TOTP는 추가 비용이 0원이면서 표준이 명확합니다. 슈퍼베이스(Supabase)를 쓰신다면 내장 MFA로 며칠이 아니라 몇 시간 만에 붙일 수 있고, 직접 서버를 굴리신다면 speakeasy 같은 라이브러리로 같은 표준을 구현하면 됩니다. 이 글은 제가 실제로 2FA를 붙이며 쓴 코드와, 한국 사용자에게 맞는 UX 고려점을 함께 정리합니다.


이 글이 필요한 사람
  • 로그인은 만들었는데 2차 인증을 어떻게 붙일지 막막한 1인 개발자
  • SMS 문자 인증과 인증 앱 TOTP 중 무엇을 골라야 할지 비용·보안으로 비교하고 싶은 분
  • 슈퍼베이스를 이미 쓰고 있어 내장 MFA로 빠르게 끝내고 싶은 분
  • 직접 만든 백엔드(Next.js·Node)에 TOTP를 라이브러리로 구현하려는 분
  • 카카오·네이버에 익숙한 한국 사용자가 헤매지 않을 2FA 화면 흐름을 고민하는 분

※ 2026년 6월 기준으로 정리했습니다. 라이브러리 버전과 슈퍼베이스 정책·가격은 수시로 바뀌므로 도입 전 공식 문서에서 최신 값을 확인하시길 권장합니다.


2차 인증·MFA·OTP, 헷갈리는 세 단어부터 정리합니다

코드를 보기 전에 용어부터 맞추겠습니다. 셋을 자주 섞어 쓰지만 층위가 다릅니다.


  • 2차 인증(2FA) — 비밀번호 하나만으로 끝내지 않고, 두 번째 증거를 한 번 더 요구하는 방식입니다. 비밀번호(아는 것)에 더해, 휴대폰 안의 인증 앱(가진 것)을 두 번째 요소로 씁니다.
  • 다중 인증(MFA) — 2차 인증을 일반화한 말입니다. 요소를 두 개 이상 쓰면 모두 MFA이고, 정확히 두 개면 2FA입니다. 슈퍼베이스 문서는 MFA라는 용어를 씁니다.
  • 일회용 비밀번호(OTP) — 한 번 쓰고 버리는 6자리 숫자 코드 자체를 가리킵니다. 이 코드를 만들어 내는 방식이 TOTP(시간 기반)와 HOTP(횟수 기반)로 갈립니다.

정리하면, 우리가 만들 것은 비밀번호 다음에 6자리 OTP를 한 번 더 받는 2FA이고, 그 OTP를 만드는 표준으로 TOTP를 쓰는 구조입니다. 아래 표로 한 번 더 비교합니다.


용어무엇을 가리키나우리 글에서의 위치
2FA요소 두 개로 본인 확인우리가 만들 기능 전체
MFA요소 둘 이상의 일반 개념슈퍼베이스 API 이름
OTP일회용 코드 그 자체사용자가 입력하는 6자리
TOTP시간으로 OTP를 만드는 방식우리가 쓸 생성 표준

한 가지 짚을 점은, 카카오 로그인이나 네이버 로그인 같은 소셜 로그인은 그 자체가 2차 인증이 아니라는 것입니다. 소셜 로그인은 비밀번호 관리를 외부에 맡기는 인증 위임이고, 그 위에 우리 서비스가 별도의 2차 요소를 더 얹을 수 있습니다. 그래서 소셜 로그인을 쓰더라도 민감한 작업 앞에 TOTP를 한 겹 더 두는 설계가 가능합니다.


2차 인증 직접 구현하기 관련 이미지 — 2차 인증·MFA·OTP, 헷갈리는 세 단어부터 정리합니다
2차 인증 직접 구현하기 관련 참고 이미지

TOTP는 인터넷 없이도 같은 코드가 맞춰지는 원리입니다

TOTP가 신기해 보이는 이유는, 인증 앱이 서버와 통신하지 않는데도 매번 같은 6자리가 맞아떨어지기 때문입니다. 비밀은 두 가지를 양쪽이 똑같이 공유한다는 데 있습니다. 하나는 등록 시점에 단 한 번 주고받은 비밀 키(secret), 다른 하나는 누구에게나 똑같은 현재 시각입니다.


동작은 이렇게 흘러갑니다. 서버가 무작위 비밀 키를 만들어 QR 코드로 보여주면, 사용자의 인증 앱이 그 키를 저장합니다. 이후 앱은 30초마다 (현재 시각 ÷ 30초)와 비밀 키를 해시 함수(HMAC-SHA1)에 넣어 6자리를 뽑아냅니다. 서버도 같은 비밀 키와 같은 시각으로 같은 계산을 하므로 결과가 일치합니다. 네트워크가 끊긴 비행기 안에서도 코드가 맞는 이유가 이것입니다.


시계 오차는 어떻게 처리하나요?
사용자 휴대폰 시각이 몇 초 어긋날 수 있어, 서버는 보통 앞뒤 한두 칸(30초 × 1~2)의 코드까지 허용합니다. 이 허용 폭을 window라고 부릅니다. 너무 넓히면 보안이 약해지고, 너무 좁히면 멀쩡한 사용자가 거부당합니다. 대부분 ±1칸이 무난합니다.

HOTP라는 변형도 있습니다. 시간 대신 카운터(몇 번째 코드인지)를 쓰는 방식인데, 카운터가 양쪽에서 어긋나기 쉬워 실무에서는 거의 TOTP를 씁니다. 우리도 TOTP만 다루겠습니다.


SMS · 인증 앱 · 보안 키, 세 가지 2차 요소를 비교하면

2차 요소로 무엇을 쓸지가 첫 갈림길입니다. 흔히 쓰는 세 가지를 비용·보안·구현 난이도로 비교하겠습니다.


방식추가 비용보안구현 난이도한국 환경 메모
SMS 문자건당 과금낮음~중간중간국내 문자 발송사 연동·발신번호 등록 필요
인증 앱(TOTP)0원높음낮음앱 설치 안내가 한국 사용자에게 다소 생소
보안 키(WebAuthn)기기 구매최고높음물리 키 보급률 낮아 일반 서비스엔 과함

SMS는 직관적이지만 약점이 분명합니다. 문자 한 건마다 발송 비용이 들고, 1인 서비스에 사용자가 늘면 그대로 고정비가 됩니다. 보안 측면에서도 유심 스와핑(번호 탈취)이나 문자 가로채기에 취약해, 국제 보안 가이드는 SMS를 권장 수단에서 점점 내리는 추세입니다. 무엇보다 국내에서 문자를 보내려면 발송 대행사를 붙이고 발신번호 사전 등록을 해야 해서, 1인 개발자에게는 설정 부담이 큽니다.


인증 앱 방식(TOTP)은 그 반대입니다. 코드를 사용자 휴대폰이 직접 계산하니 발송 비용이 0원이고, 표준이 공개돼 있어 어떤 인증 앱과도 호환됩니다. 제가 직접 붙여 본 결과 구현도 가장 단순했습니다. 보안 키(WebAuthn·하드웨어 키)는 가장 강력하지만 물리 기기가 필요해, 사내 어드민처럼 사용자가 한정된 곳이 아니면 일반 서비스에는 과한 선택입니다.


제 결론
1인 서비스에서 2차 인증의 기본값은 인증 앱 TOTP입니다. 비용 0원, 표준 명확, 구현 단순의 세 박자가 맞습니다. SMS는 사용자가 인증 앱을 도저히 못 쓰는 경우의 보조 수단으로만 두는 편이 합리적입니다.

슈퍼베이스 MFA로 2FA 붙이기 — 등록부터 검증까지 코드

슈퍼베이스를 쓰고 계신다면 이게 가장 빠른 길입니다. 비밀 키 생성·QR 코드·검증·세션 등급 관리를 슈퍼베이스가 다 처리하므로, 우리는 화면 흐름만 붙이면 됩니다. 저는 tech.ambitstock 본체를 Next.js와 슈퍼베이스, 버셀(Vercel)로 1인 운영하고 있어서 어드민 보호에 이 방식을 그대로 썼습니다.


전체 흐름은 세 단계입니다. (1) enroll로 새 요소를 등록하며 QR을 받아 사용자에게 보여주고, (2) challenge로 검증 요청을 만든 뒤, (3) 사용자가 입력한 6자리를 verify로 확인합니다. 먼저 등록 부분입니다.


2차 인증 직접 구현하기 관련 이미지 — TOTP는 인터넷 없이도 같은 코드가 맞춰지는 원리입니다
2차 인증 직접 구현하기 관련 참고 이미지
슈퍼베이스 MFA 요소 등록 — QR 코드 받기 (브라우저)
import { createClient } from '@supabase/supabase-js' const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ) // 1) 새 TOTP 요소 등록 — 비밀 키와 QR 코드(SVG)를 돌려준다 async function enrollTotp() { const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp', friendlyName: '내 인증 앱', }) if (error) throw error // data.id : 이 요소의 식별자 (challenge/verify 에 필요) // data.totp.qr_code : QR 코드 SVG (그대로 화면에 그리면 됨) // data.totp.secret : 수동 입력용 비밀 키 (QR 못 찍을 때 대비) return data }

등록으로 받은 qr_code를 화면에 띄우면 사용자가 인증 앱으로 찍습니다. 그다음 사용자가 앱에 뜬 6자리를 입력하면, 그 값으로 검증을 마쳐 요소를 활성화합니다. 검증 단계는 challengeverify가 한 쌍입니다.


6자리 코드 검증 — 등록 확정 (challenge + verify)
// 2) 사용자가 입력한 6자리(code)로 요소를 검증해 활성화한다 async function verifyTotp(factorId, code) { // 2-1) 검증 요청 생성 const { data: challenge, error: cErr } = await supabase.auth.mfa.challenge({ factorId }) if (cErr) throw cErr // 2-2) 사용자가 입력한 코드로 검증 const { data, error } = await supabase.auth.mfa.verify({ factorId, challengeId: challenge.id, code, // 사용자가 인증 앱에서 읽은 6자리 숫자 }) if (error) throw error // 검증 성공 시 세션 등급이 aal2 로 올라간다 return data }

핵심 개념이 AAL(Authenticator Assurance Level)입니다. 비밀번호만으로 로그인한 세션은 aal1, 여기에 2차 인증까지 통과하면 aal2가 됩니다. 그래서 민감한 페이지(결제·관리자 화면)는 aal2일 때만 접근을 허용하도록 막으면 됩니다. 로그인 직후 현재 등급을 확인하는 코드는 다음과 같습니다.


현재 세션의 인증 등급(AAL) 확인 — 2FA 요구 분기
// 3) 현재 등급과 도달 가능한 등급을 비교해 2FA 화면을 띄울지 결정 async function checkMfaStep() { const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel() if (error) throw error const { currentLevel, nextLevel } = data // currentLevel: 지금 세션 등급 (aal1 / aal2) // nextLevel: 이 사용자가 도달해야 하는 등급 if (currentLevel === 'aal1' && nextLevel === 'aal2') { return 'show_2fa' // 2차 인증 입력 화면으로 보낸다 } if (currentLevel === 'aal2') { return 'ok' // 이미 2차 인증 통과 — 통과시킨다 } return 'no_mfa' // 이 사용자는 2FA 미설정 }
RLS와 함께 쓰면 더 단단해집니다. 슈퍼베이스의 행 수준 보안(Row Level Security) 정책에 인증 등급 조건을 넣으면, 데이터베이스 차원에서 aal2가 아닌 요청을 차단할 수 있습니다. 화면에서만 막는 것과 달리 API를 직접 호출해도 뚫리지 않으므로, 정말 민감한 테이블에는 RLS 정책으로 한 겹 더 두시길 권합니다.

슈퍼베이스 없이 직접 만든다면 speakeasy로 TOTP 구현

인증을 직접 만든 백엔드(Node·Next.js API 라우트)라면 표준 라이브러리로 같은 결과를 얻을 수 있습니다. 검증된 조합은 비밀 키 생성·검증용 speakeasy와 QR 이미지 생성용 qrcode입니다. 둘 다 의존성이 가볍고 오래 검증됐습니다.


구현은 슈퍼베이스 흐름과 똑같이 (1) 비밀 키 발급, (2) QR 표시, (3) 코드 검증 세 단계입니다. 먼저 등록 시 비밀 키를 만들고 사용자에게 보여줄 QR을 생성하는 서버 코드입니다.


2차 인증 직접 구현하기 관련 이미지 — SMS · 인증 앱 · 보안 키, 세 가지 2차 요소를 비교하면
2차 인증 직접 구현하기 관련 참고 이미지
speakeasy로 비밀 키 발급 + QR 생성 (설치 및 서버)
# 설치 npm install speakeasy qrcode # (아래는 서버 측 자바스크립트 코드)
등록 시 비밀 키 생성과 QR DataURL 만들기
import speakeasy from 'speakeasy' import QRCode from 'qrcode' // 1) 사용자별 비밀 키 생성 — base32 값을 DB에 안전하게 저장 export async function createTotpSecret(userEmail) { const secret = speakeasy.generateSecret({ name: 'MyService (' + userEmail + ')', // 인증 앱에 표시될 이름 length: 20, }) // 인증 앱이 읽을 QR 이미지를 DataURL 로 변환 const qrDataUrl = await QRCode.toDataURL(secret.otpauth_url) return { base32: secret.base32, // DB 에 저장 (절대 클라이언트로 노출 금지) qrDataUrl, // <img src={qrDataUrl}> 로 표시 } }

사용자가 QR을 등록한 뒤 입력한 6자리를, 저장해 둔 비밀 키와 맞춰 검증합니다. 여기서 시계 오차를 흡수하는 window 값이 등장합니다.


입력한 6자리 코드 검증 (window로 시계 오차 허용)
import speakeasy from 'speakeasy' // 2) 사용자가 입력한 코드 검증 export function verifyTotpCode(savedBase32, userInput) { return speakeasy.totp.verify({ secret: savedBase32, encoding: 'base32', token: userInput, // 사용자가 친 6자리 window: 1, // 앞뒤 30초 한 칸씩 허용 (±1) }) // true 면 통과, false 면 거부 }

여기서 보안상 절대 빠뜨리면 안 되는 두 가지가 있습니다. 첫째, 비밀 키(base32)는 서버 데이터베이스에만 저장하고 클라이언트로 절대 보내지 않습니다. 둘째, 검증에 성공한 코드를 짧은 시간 동안 재사용하지 못하게 막아야 합니다. TOTP 코드는 30초간 유효한데, 공격자가 같은 코드를 그 안에 가로채 재전송하면 통과될 수 있기 때문입니다. 마지막으로 쓴 코드의 시각을 기록해 같은 칸의 코드를 두 번 받지 않도록 거르면 안전합니다.


휴대폰을 잃어버리면? 복구 코드를 반드시 함께 발급합니다

2차 인증을 붙이면 새로운 사고가 생깁니다. 사용자가 휴대폰을 바꾸거나 잃어버려 인증 앱이 사라지면, 본인인데도 영영 못 들어오는 잠금 상태에 빠집니다. 이걸 대비하지 않으면 2FA는 보안 기능이 아니라 고객센터 문의 폭탄이 됩니다.


표준 해법이 복구 코드(recovery code)입니다. 등록을 마치는 순간, 한 번만 쓸 수 있는 일회용 코드 8~10개를 한꺼번에 발급해 사용자가 안전한 곳에 보관하게 합니다. 인증 앱을 잃으면 이 코드 중 하나로 로그인해 2FA를 다시 설정합니다. 구현 시 지켜야 할 원칙은 다음과 같습니다.


  • 발급은 등록 직후 단 한 번만 보여줍니다. 화면을 닫으면 다시 못 봅니다.
  • 데이터베이스에는 원문이 아니라 해시로 저장합니다. 비밀번호처럼 단방향 해시(bcrypt 등)로 보관해야 유출돼도 안전합니다.
  • 한 번 쓴 코드는 즉시 폐기합니다. 재사용을 막아야 일회용의 의미가 삽니다.
  • 남은 개수를 사용자에게 알려 떨어지기 전에 재발급하도록 안내합니다.

제가 겪은 실수
처음엔 복구 코드 없이 2FA만 붙였다가, 테스트 계정에서 인증 앱을 지운 순간 그 계정으로 다시 못 들어가 데이터베이스에서 직접 요소를 지워야 했습니다. 1인 서비스라 제가 직접 손볼 수 있었지 실제 사용자였다면 그대로 이탈입니다. 복구 코드는 선택이 아니라 필수라고 보시면 됩니다.
2차 인증 직접 구현하기 관련 이미지 — 슈퍼베이스 MFA로 2FA 붙이기 — 등록부터 검증까지 코드
2차 인증 직접 구현하기 관련 참고 이미지

한국 사용자를 위한 2FA 화면, 카카오·네이버에 익숙한 사람 기준으로

기술이 맞아도 화면이 낯설면 사용자는 중간에 포기합니다. 특히 한국 사용자는 카카오·네이버·토스의 간편 인증에 익숙해서, 인증 앱을 따로 깔아 QR을 찍는 흐름 자체가 생소할 수 있습니다. 제가 화면을 다듬으며 신경 쓴 포인트를 정리합니다.


  • 인증 앱 설치부터 손을 잡아줍니다. "인증 앱을 여세요"로 시작하지 말고, 구글 OTP·Microsoft Authenticator 등 어떤 앱을 어디서 받는지 링크와 함께 먼저 안내합니다. 앱이 없는 사용자가 대부분이라는 전제로 화면을 짜야 합니다.
  • QR을 못 찍는 경우의 수동 입력 키를 항상 함께 보여줍니다. PC에서 로그인하는 사용자는 같은 화면의 QR을 휴대폰으로 못 찍습니다. base32 비밀 키를 복사 버튼과 함께 두면 막힘이 줄어듭니다.
  • 6자리 입력칸은 숫자 키패드로 띄웁니다. 모바일에서 inputmode="numeric"을 주면 자판이 바로 숫자로 떠서 입력이 빨라집니다.
  • 30초 갱신을 설명합니다. 코드가 자꾸 바뀌는 걸 오류로 오해하는 분이 있어, "코드는 30초마다 바뀝니다"라는 한 줄을 입력칸 옆에 둡니다.
  • 복구 코드 보관 안내를 한국어로 분명히 합니다. 캡처하거나 출력해 두라고 구체적으로 말해야 나중에 잠금 사고를 줄입니다.

로그인 위임은 카카오·네이버 같은 소셜 로그인에 맡기고, 그 위에 결제나 관리자 작업처럼 정말 위험한 행동 앞에서만 2차 인증을 한 번 더 요구하는 설계가 한국 사용자 정서에 잘 맞았습니다. 모든 로그인마다 OTP를 강제하면 이탈이 커지니, 위험한 구간에만 거는 단계적 적용을 권합니다.


2FA 구현에서 자주 터지는 함정 5가지와 해결

케이스 → 원인 → 해결 순으로 자주 막히는 지점을 정리합니다. 대부분 처음 붙일 때 한 번씩 겪는 것들입니다.


증상원인해결
코드가 계속 틀렸다고 나온다서버 시각이 어긋남서버 시각을 NTP로 동기화, window 1로 완화
QR을 찍어도 앱에 안 들어간다otpauth URL 형식 오류라이브러리가 만든 otpauth_url 그대로 사용
한 코드로 두 번 통과된다재사용 방지 누락마지막 사용 칸 기록해 재입력 차단
앱 잃으면 영영 못 들어온다복구 수단 없음복구 코드 발급·해시 저장 필수
비밀 키가 클라이언트에 보인다검증을 프런트에서 처리비밀 키와 검증은 반드시 서버에서

특히 첫 번째와 세 번째가 흔합니다. 서버 시각 문제는 클라우드라면 대개 동기화돼 있지만, 직접 띄운 가상 서버나 로컬 도커에서 시각이 밀려 있으면 멀쩡한 코드가 계속 거부됩니다. 재사용 방지는 코드 줄 수가 얼마 안 돼 빠뜨리기 쉬운데, 보안의 핵심이라 반드시 넣으셔야 합니다.


참고 자료


SMS 문자 인증과 인증 앱 TOTP 중 무엇이 더 안전한가요?
인증 앱 TOTP가 더 안전합니다. SMS는 유심 스와핑으로 번호 자체를 탈취당하거나 문자가 가로채일 수 있는 반면, TOTP는 비밀 키가 사용자 휴대폰 안에만 있어 전송 과정에서 새지 않습니다. 비용도 TOTP는 0원이지만 SMS는 발송 건마다 과금되어, 1인 서비스에는 TOTP가 보안과 비용 양쪽에서 유리합니다.
슈퍼베이스를 안 쓰는데도 2FA를 쉽게 붙일 수 있나요?
네, 직접 만든 백엔드라면 speakeasy와 qrcode 같은 표준 라이브러리로 같은 TOTP를 구현하면 됩니다. 비밀 키 생성·QR 표시·6자리 검증 세 단계만 처리하면 되고, 의존성이 가벼워 Next.js API 라우트나 Node 서버에 무리 없이 붙습니다. 표준이 공개돼 있어 어떤 인증 앱과도 호환됩니다.
인증 앱을 깔린 휴대폰을 잃어버리면 어떻게 들어가나요?
복구 코드로 들어갑니다. 2FA 등록을 마치는 순간 일회용 복구 코드 8~10개를 발급해 사용자가 안전한 곳에 보관하게 해야 합니다. 인증 앱이 사라지면 이 코드 중 하나로 로그인한 뒤 2FA를 다시 설정합니다. 복구 코드를 발급하지 않으면 본인도 영영 못 들어오는 잠금 사고가 생기므로 반드시 함께 만들어야 합니다.
코드가 자꾸 틀렸다고 나오는데 원인이 뭔가요?
대부분 서버 시각이 어긋난 경우입니다. TOTP는 시각을 기준으로 계산하기 때문에 서버 시계가 몇십 초 밀리면 멀쩡한 코드도 거부됩니다. 서버 시각을 NTP로 동기화하고, 검증 옵션에서 window를 1로 두어 앞뒤 30초 한 칸씩 허용하면 휴대폰과 서버의 작은 오차를 흡수할 수 있습니다.
카카오·네이버 소셜 로그인을 쓰면 2차 인증은 따로 안 해도 되나요?
소셜 로그인은 비밀번호 관리를 외부에 위임하는 인증일 뿐 그 자체가 2차 인증은 아닙니다. 결제나 관리자 작업처럼 위험한 행동 앞에서는 소셜 로그인 위에 TOTP를 한 겹 더 얹는 설계가 안전합니다. 모든 로그인에 강제하면 이탈이 커지니, 민감한 구간에만 거는 단계적 적용을 권합니다.
비밀 키는 어디에 저장해야 하나요?
비밀 키는 반드시 서버 데이터베이스에만 저장하고 클라이언트로는 절대 보내지 않습니다. 검증도 서버에서 처리해야 합니다. 프런트엔드에서 검증하면 비밀 키가 브라우저에 노출되어 누구나 코드를 위조할 수 있습니다. 슈퍼베이스 MFA를 쓰면 비밀 키 보관과 검증을 슈퍼베이스가 서버 측에서 대신 처리하므로 이 부분을 직접 신경 쓰지 않아도 됩니다.
2차 인증MFATOTPOTP슈퍼베이스Supabasespeakeasy인증 보안Next.js

관련 도구

Supabase인프라

PostgreSQL 기반 오픈소스 BaaS. 인증, DB, 스토리지, 실시간을 한번에 제공.

LovableAI 코딩 도구

채팅만으로 풀스택 웹앱을 생성하는 클라우드 AI 앱 빌더. 코드를 직접 안 짜도 프론트엔드부터 슈퍼베이스 백...

함께 보면 좋은 문제 해결

관련 포스트