반응형

서버리스 (Vercel/Netlify)

  • 요청이 올 때만 함수가 잠깐 깨어나 실행 → 끝나면 사라짐
  • 장점: 안 쓸 때 비용 0, 자동 확장
  • 단점: 상시 연결(WebSocket)·긴 작업·백그라운드 크론 어려움

상시 서버 (Render/Railway)

  • 컨테이너가 24시간 계속 켜져 있음 (전통적인 서버 방식)
  • 장점: Express 그대로 실행, 무엇이든 가능
  • 단점: 안 써도 켜져 있으니 비용 발생(무료 티어는 슬립 걸림)

그래서 경계가 흐릿합니다

  • Vercel도 서버리스로 백엔드를 돌릴 수 있고,
  • Render/Railway도 정적 프론트를 올릴 수 있습니다.

서버리스가 아닌 상시 서버가 꼭 필요한 기능

상시 서버가 꼭 필요한 기능은 서버리스 환경의 제약사항(제한된 실행 시간, 콜드 스타트, 상태 저장 불가)으로 인해 발생합니다. 주로 15분 이상의 장기 실행 작업, 지속적인 클라이언트 연결 처리, 실시간 데이터 스트리밍에 필수적입니다. [1, 2]
대표적으로 상시 서버가 반드시 필요한 기능은 다음과 같습니다.
  • 실시간 양방향 통신 및 스트리밍: 소켓(WebSocket)이나 WebRTC를 활용한 실시간 채팅, 영상 스트리밍, 원격 제어, 멀티플레이어 게임 서버 등은 클라이언트와 지속적인 연결(Stateful)을 유지해야 하므로 상시 서버가 필수적입니다. [1]
  • 장기 실행 백그라운드 작업: 1회 실행 제한 시간(예: AWS Lambda 기준 최대 15분)을 초과하는 대용량 데이터 처리, 배치 작업(Batch Processing), 머신러닝 모델 학습 및 데이터 백업 등은 상시 구동되는 서버가 필요합니다.
  • 상태 정보(State) 유지 및 캐싱: 로그인 세션 정보나 실시간 게임 상태 등 지속적으로 메모리에 유지되어야 하는 상태 값을 다루는 기능입니다. 서버리스는 요청마다 상태가 초기화되기 때문에 연속적인 데이터 관리에 불리합니다. [1, 2]
  • 엄격한 실시간 응답 (지연 시간 최소화): 서버리스는 첫 요청 시 서버가 초기화되는 '콜드 스타트(Cold Start)'로 인해 응답 지연이 발생할 수 있습니다. 금융 거래, 자율주행 데이터 처리, 실시간 입찰 시스템처럼 0.001초의 지연도 치명적인 기능은 상시 서버를 사용해야 합니다. [1]
  • 특정 OS 및 하드웨어 제어: GPU가 반드시 필요한 작업이거나, 시스템 레벨의 커널 설정 및 특정 운영체제(OS)의 라이브러리를 직접 수정해야 하는 기능은 클라우드 제공업체의 통제를 받는 서버리스 환경에서 구현할 수 없습니다.
각자의 서비스 목적과 트래픽 규모에 맞는 아키텍처를 선택하기 위해 클라우드 서비스 제공업체에서 제공하는 AWS 컴퓨팅 서비스Google Cloud 컴퓨팅 엔진 의 옵션 및 특징을 비교해 볼 수 있습니다.
 

 


Vercel/Netlify 같은 정적 호스팅, 백엔드는 별도 서버(Render/Railway 등)???
네 곳 모두 코드를 올리면 인터넷에 띄워주는 호스팅입니다. 차이는 어떤 실행 방식을 주력으로 미느냐입니다.

서비스 주력 방식 잘 맞는 것
Vercel / Netlify 정적 파일 + 서버리스(요청 올 때만 실행) 프론트엔드, Next.js, 가벼운 API
Render / Railway 상시 켜진 서버(항상 돌아감) Express, DB, WebSocket, 크론, 무거운 작업
반응형
반응형

두 개의 독립적인 관문이 있고, anon SELECT 는 둘 다 통과해야 합니다. RLS 는 그 중 하나일 뿐이에요.

판단 순서

   anon key로 SELECT 요청
          ↓
  ① GRANT SELECT ON table TO anon    ← 테이블 레벨 권한 (SQL 표준)
     ↓ 없으면 → permission denied 에러
  ② RLS enabled?
     ├─ NO  → 통과, 전체 row 반환
     └─ YES → ③ 이 SELECT 를 허용하는 정책이 있나?
                 ├─ 없음 → 0건 반환 (에러 아님!)
                 └─ 있음 → 정책의 USING 조건 만족하는 row 만 반환

진리표

RLS anon 에 SELECT GRANT SELECT 정책 결과
OFF (무관) permission denied for table ... 에러
OFF (무관) 전체 row 반환
ON (무관) permission denied 에러
ON 없음 ⚠️ 빈 배열 [] (에러 없음, row 만 0건)
ON USING (true) 같은 관대한 정책 ✅ 전체 row 반환
ON USING (user_id = auth.uid()) 같은 조건 정책 정책 조건 통과한 row 만 반환

핵심 함정: RLS on + 정책 없음 = 0건 (에러 없음)

이게 지난번 getResidence 404 사건의 원인이었어요. 앱 입장에서 보면:

const { data, error } = await supabase.from('decozip_residence')...
// error = null   ← 에러 아님!
// data  = null   ← 그냥 row 가 없다고 나옴

앱은 "row 가 존재하지 않는다"로 해석 → notFound() → 404. DB 에는 row 가 실제로 있어도 그렇게 됩니다.

이래서 "RLS 는 fail-closed(기본 거부)" 라고 표현해요. SQL 의 기본 조회 결과와 자연스럽게 통합돼서 티가 안 남.

Supabase 특수 사항

Supabase 는 프로젝트 생성 시 몇 가지 자동 설정을 해두어서 헷갈릴 수 있어요:

  1. 자동 GRANT: 새 테이블 생성 시 anon, authenticated, service_role 에 CRUD 를 자동으로 GRANT. 그래서 ① 관문은 자동 통과하는 경우가 대부분.
  2. 자동 RLS ON: Dashboard 로 테이블 생성하면 RLS 가 켜진 상태로 시작하는 게 요즘 기본값 (2023 이후). SQL 로 CREATE TABLE 하면 꺼진 상태.
  3. 경고 배너: RLS 를 껐거나 정책 없이 켜두면 Dashboard 에 "Insecure" 배지가 표시됨.

지난번 문제도 이 조합에서 발생: ALTER TABLE ... ADD COLUMN 하거나 Dashboard 에서 컬럼 편집하면 종종 RLS 스위치가 자동으로 켜져요. 그 순간부터 정책 없으니 anon 조회 결과가 다 0건이 되고, 앱은 404.

결정적 차이 요약

  • GRANT = "이 role 이 이 테이블을 건드릴 수 있는가?" — 테이블 단위
  • RLS = "이 role 이 이 특정 row 를 볼/쓸 수 있는가?" — row 단위

GRANT 는 SQL 표준의 20년 넘은 개념, RLS 는 PostgreSQL 이 나중에 얹은 세밀한 필터. 둘은 각기 다른 층에서 동작하며 AND 조건으로 결합됩니다.

실전 검증 명령

-- ① 관문 (GRANT 상태)
SELECT has_table_privilege('anon', 'public.decozip_residence', 'SELECT') AS anon_can_select;

-- ② + ③ 관문 (RLS 상태 + 정책 개수)
SELECT
  c.relrowsecurity AS rls_on,
  (SELECT COUNT(*) FROM pg_policies p
    WHERE p.schemaname='public' AND p.tablename='decozip_residence'
      AND p.cmd IN ('SELECT','ALL')
      AND ('anon' = ANY(p.roles) OR 'public' = ANY(p.roles))
  ) AS anon_select_policies
FROM pg_class c
WHERE c.oid = 'public.decozip_residence'::regclass;
  • anon_can_select = true + rls_on = false전 row 조회 OK
  • anon_can_select = true + rls_on = true + anon_select_policies = 00건 (지난 404 상황)
  • anon_can_select = true + rls_on = true + anon_select_policies ≥ 1정책 조건대로
  • anon_can_select = false에러 (GRANT 부터 문제)
반응형
반응형
반응형
반응형

진단 SQL (Supabase SQL Editor)

-- 1) RLS 켜져 있는지 확인
SELECT
  schemaname, tablename,
  rowsecurity                                        AS rls_enabled,
  (SELECT COUNT(*) FROM pg_policies p
   WHERE p.schemaname='public' AND p.tablename='decozip_residence') AS policy_count
FROM pg_tables
WHERE schemaname='public' AND tablename='decozip_residence';

-- 2) 정책이 있으면 어떤 정책인지
SELECT policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname='public' AND tablename='decozip_residence';

rls_enabled = true 이고 policy_count = 0 이면 확정

해결방안

A. RLS 끄기 (가장 빠름, 현재 코드 그대로 동작)

ALTER TABLE public.decozip_residence DISABLE ROW LEVEL SECURITY;
운영 초기 단계고 anon 키가 유출된 상태가 아니면 이게 가장 간단. 다른 테이블들도 대부분 RLS 없이 동작 중이니 일관성도 유지됩니다.

B. RLS 유지 + anon SELECT 정책 추가

ALTER TABLE public.decozip_residence ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS residence_select_all ON public.decozip_residence;
CREATE POLICY residence_select_all ON public.decozip_residence
FOR SELECT USING (true);

-- INSERT/UPDATE 도 필요하면 각각 별도 정책 (신규 등록·수정 화면용)
DROP POLICY IF EXISTS residence_insert_all ON public.decozip_residence;
CREATE POLICY residence_insert_all ON public.decozip_residence
FOR INSERT WITH CHECK (true);

DROP POLICY IF EXISTS residence_update_all ON public.decozip_residence;
CREATE POLICY residence_update_all ON public.decozip_residence
FOR UPDATE USING (true) WITH CHECK (true);
decozip_chat_message 이 이 패턴으로 되어 있어요 (data/public.decozip_chat_message.sql:42-50).

반응형
반응형

REPLACE(l.display_name, ' ', '')

TRANSLATE(l.display_name, ' /-·.', '')

특수문자 여러개 빼고 비교하고 싶다면 TRANSLATE를 사용하자!

반응형

'db' 카테고리의 다른 글

postgresql (IS NOT TRUE) === (!=TRUE or IS NULL )  (0) 2026.07.01
[postgresql] timestamp vs timestamptz  (0) 2026.06.25
DBeaver mariaDB 연결  (0) 2024.05.10
mysql 모든 table, key, index...etc 확인  (0) 2022.05.19
mysql change column error  (0) 2022.04.28
반응형

!= true쓰지 말고 is not true로 사용하자

반응형
반응형

CREATE OR REPLACE VIEW public.v_leads -> view는 삭제하고 재생성해야함, 삭제안하고 시도하면 에러남 ( ERROR: cannot change name of view column "_install_date" to "status_kr" , 기존이랑 컬럼수가 달라져서 그런것인지...?)
AS SELECT

CASE b.status::varchar -> status가 enum이라면 casting해줘야함, 안하면 에러남
WHEN 'consultation_pending' THEN '상담 전'
WHEN 'consultation_ended' THEN '상담종료'
WHEN 'no_response' THEN '반응 무'
WHEN 'qualified' THEN '가능고객'
WHEN 'hot_lead' THEN '가망고객'
WHEN 'quote_sent' THEN '견적 발송
WHEN 'contracted_no_date' THEN '시공일미정'
WHEN 'contracted' THEN '계약 완료'
WHEN 'completed' THEN '시공 완료'
ELSE b.status::varchar
END AS status_kr,

이하 생략

반응형
반응형

const geo_country = h.get('x-vercel-ip-country') || h.get('cf-ipcountry') || null

동작 방식

출처 헤더 조건
Vercel x-vercel-ip-country Vercel 배포 시 Edge Network가 자동 주입
Cloudflare cf-ipcountry Cloudflare 프록시 통과 시 자동 주입

vercel, cloudflare 외에 다른 환경이라면
AWS / Netlify / 자체 서버: 위 헤더들이 자동으로 안 와요. 그 경우엔 IP → 국가 변환을 직접 해야 합니다.

무료 옵션:

  • geoip-lite npm 패키지 (DB 파일 번들, 오프라인 lookup)
  • ipapi.co / ip-api.com 같은 외부 API (요청당 호출, rate limit 있음)
  • MaxMind GeoLite2 DB 다운로드해서 직접 lookup
반응형
반응형

deviceType: bot|mobile|tablet|desktop|unknown

UPDATE public.decozip_ad_visit
SET device_type = CASE
  WHEN user_agent IS NULL OR user_agent = '' THEN 'unknown'
  WHEN user_agent ~* 'bot|crawler|spider|crawling|facebookexternalhit|slackbot|whatsapp|kakaotalk-scrap'
    THEN 'bot'
  WHEN user_agent ~* 'iPhone|iPod|Android.*Mobile|Mobi|BlackBerry|IEMobile|Opera Mini|webOS'
    THEN 'mobile'
  WHEN user_agent ~* 'iPad|Tablet|Android(?!.*Mobile)|Kindle|Silk|PlayBook'
    THEN 'tablet'
  ELSE 'desktop'
END
WHERE device_type IS NULL;
export function detectDeviceType(ua: string | null | undefined): AdVisitDeviceType {
  if (!ua) return 'unknown'
  if (/bot|crawler|spider|crawling|facebookexternalhit|slackbot|whatsapp|kakaotalk-scrap/i.test(ua)) return 'bot'
  if (/iPhone|iPod|Android.*Mobile|Mobi|/**
 * user-agent 문자열에서 브라우저 종류를 추출.
 * 한국형 인앱(KAKAO/Instagram/FB/당근/Whale) 을 일반 브라우저보다 먼저 검사.
 * (Chrome UA 에 Safari 가 포함되므로 Safari 는 Chrome 보다 뒤에)
 * (data/migration_2026_06_25_ad_visit_browser_geo.sql 의 백필 규칙과 동일)
 */
export function detectBrowser(ua: string | null | undefined): AdVisitBrowser {
  if (!ua) return 'unknown'
  if (/KAKAOTALK/i.test(ua)) return 'kakao'
  if (/Instagram/i.test(ua)) return 'instagram'
  if (/FBAN|FBAV|FB_IAB/i.test(ua)) return 'facebook'
  if (/Daangn|DaangnApp/i.test(ua)) return 'daangn'
  if (/Whale\//i.test(ua)) return 'whale'
  if (/SamsungBrowser/i.test(ua)) return 'samsung'
  if (/Edg\/|EdgA\/|EdgiOS\//i.test(ua)) return 'edge'
  if (/Firefox\/|FxiOS\//i.test(ua)) return 'firefox'
  if (/Chrome\/|CriOS\//i.test(ua)) return 'chrome'
  if (/Safari\//i.test(ua)) return 'safari'
  return 'other'
}|Opera Mini|webOS/i.test(ua)) return 'mobile'
  if (/iPad|Tablet|Kindle|Silk|PlayBook/i.test(ua)) return 'tablet'
  if (/Android/i.test(ua) && !/Mobile/i.test(ua)) return 'tablet'
  return 'desktop'
}

browser:

UPDATE public.decozip_ad_visit
SET browser = CASE
  WHEN user_agent IS NULL OR user_agent = ''       THEN 'unknown'
  WHEN user_agent ~* 'KAKAOTALK'                   THEN 'kakao'
  WHEN user_agent ~* 'Instagram'                   THEN 'instagram'
  WHEN user_agent ~* 'FBAN|FBAV|FB_IAB'            THEN 'facebook'
  WHEN user_agent ~* 'Daangn|DaangnApp'            THEN 'daangn'
  WHEN user_agent ~* 'Whale/'                      THEN 'whale'
  WHEN user_agent ~* 'SamsungBrowser'              THEN 'samsung'
  WHEN user_agent ~* 'Edg/|EdgA/|EdgiOS/'          THEN 'edge'
  WHEN user_agent ~* 'Firefox/|FxiOS/'             THEN 'firefox'
  WHEN user_agent ~* 'Chrome/|CriOS/'              THEN 'chrome'
  WHEN user_agent ~* 'Safari/'                     THEN 'safari'
  ELSE 'other'
END
WHERE browser IS NULL;
/**
 * user-agent 문자열에서 브라우저 종류를 추출.
 * 한국형 인앱(KAKAO/Instagram/FB/당근/Whale) 을 일반 브라우저보다 먼저 검사.
 * (Chrome UA 에 Safari 가 포함되므로 Safari 는 Chrome 보다 뒤에)
 * (data/migration_2026_06_25_ad_visit_browser_geo.sql 의 백필 규칙과 동일)
 */
export function detectBrowser(ua: string | null | undefined): AdVisitBrowser {
  if (!ua) return 'unknown'
  if (/KAKAOTALK/i.test(ua)) return 'kakao'
  if (/Instagram/i.test(ua)) return 'instagram'
  if (/FBAN|FBAV|FB_IAB/i.test(ua)) return 'facebook'
  if (/Daangn|DaangnApp/i.test(ua)) return 'daangn'
  if (/Whale\//i.test(ua)) return 'whale'
  if (/SamsungBrowser/i.test(ua)) return 'samsung'
  if (/Edg\/|EdgA\/|EdgiOS\//i.test(ua)) return 'edge'
  if (/Firefox\/|FxiOS\//i.test(ua)) return 'firefox'
  if (/Chrome\/|CriOS\//i.test(ua)) return 'chrome'
  if (/Safari\//i.test(ua)) return 'safari'
  return 'other'
}

os

UPDATE public.decozip_ad_visit
SET os = CASE
  WHEN user_agent IS NULL OR user_agent = ''       THEN 'unknown'
  WHEN user_agent ~* 'Android'                     THEN 'android'
  WHEN user_agent ~* 'iPhone|iPad|iPod|iOS|CriOS|FxiOS|EdgiOS' THEN 'ios'
  WHEN user_agent ~* 'Windows NT|Windows Phone|WPDesktop' THEN 'windows'
  WHEN user_agent ~* 'Mac OS X|Macintosh'          THEN 'macos'
  WHEN user_agent ~* 'BlackBerry|BB10|RIM Tablet'  THEN 'blackberry'
  WHEN user_agent ~* 'Linux|X11'                   THEN 'linux'
  ELSE 'other'
END
WHERE os IS NULL;
export function detectBrowser(ua: string | null | undefined): AdVisitBrowser {
  if (!ua) return 'unknown'
  if (/KAKAOTALK/i.test(ua)) return 'kakao'
  if (/Instagram/i.test(ua)) return 'instagram'
  if (/FBAN|FBAV|FB_IAB/i.test(ua)) return 'facebook'
  if (/Daangn|DaangnApp/i.test(ua)) return 'daangn'
  if (/Whale\//i.test(ua)) return 'whale'
  if (/SamsungBrowser/i.test(ua)) return 'samsung'
  if (/Edg\/|EdgA\/|EdgiOS\//i.test(ua)) return 'edge'
  if (/Firefox\/|FxiOS\//i.test(ua)) return 'firefox'
  if (/Chrome\/|CriOS\//i.test(ua)) return 'chrome'
  if (/Safari\//i.test(ua)) return 'safari'
  return 'other'
}
반응형
반응형

timestamptz로 저장하면 아래와 같이 utc정보를 같이 저장하게 됨

+0900 -> 이거보고 한국시간 기준이라고 판단하는 게 좋을 듯

  • timestamp vs timestamptz
타임존 고려 ❌ (고려 안 함) ⭕ (UTC로 변환하여 저장)
저장 값 입력된 "문자열" 그대로 UTC 기준의 절대 시간
조회 시 어느 지역에서든 동일 조회하는 지역의 시간대로 자동 변환

DB내부적으로는 다 utc로 저장하되
조회할때는 지역시간대에 맞춰서 보여주는듯

 

반응형

+ Recent posts