Devin.KR

문자열과 정규식 - 한글을 안전하게

개발자KR 조회 0

이 장에서 배우는 것

빵집 예약 사이트에는 손님이 직접 타이핑한 글이 들어온다. 이름 앞뒤에 전각 공백이 붙어 있고, 전화번호는 하이픈이 있기도 없기도 하며, 메모에는 줄바꿈이 섞인다. 이 글은 한글이 들어간 문자열을 PHP가 어떻게 다루는지 알아야 안전하게 처리할 수 있다. 이 장에서는 글자 단위로 문자열을 다루는 mb_* 함수, 숫자와 문자열을 원하는 모양으로 찍는 sprintf와 number_format, 패턴으로 문자열을 검사하고 바꾸는 preg_match와 preg_replace를 배운다. 마지막에는 이 도구들을 묶어 입력을 정리하는 함수를 만든다.

  • 바이트 수와 글자 수, 화면 너비가 서로 다르다는 것을 설명하고 mb_strlen, mb_substr, mb_strwidth, mb_strimwidth를 구분해 쓴다.
  • sprintf와 number_format으로 주문번호와 금액을 정해진 모양으로 만든다.
  • 정규식의 기본 요소와 u 플래그를 이해하고 preg_match, preg_match_all, preg_replace로 검사와 치환을 한다.
  • 사용자 입력을 검사하고 정리하는 순서(UTF-8 검사, 공백 정리, 형식 검사)를 함수로 만든다.

문제 상황

예약 주문 폼으로 다음과 같은 값이 들어왔다고 하자.

  • 이름: 앞에 전각 공백(한글 입력기에서 나오는 넓은 빈칸)이 붙은 김민수
  • 전화번호: 010 1234-5678
  • 주문 내용: 소금빵 x4, 바게트*2, 마카롱 x 2
  • 메모: 공백과 줄바꿈이 여러 개 섞인 문장

이 값들로 해야 할 일은 여러 가지다. 이름의 앞뒤 공백을 지우고, 전화번호를 010-1234-5678 꼴로 통일하고, 주문 내용을 품목과 수량으로 쪼개고, 영수증에는 이름을 김*수처럼 가려 찍고, 금액에는 세 자리마다 쉼표를 넣고, 목록 화면에서는 긴 메모를 잘라 보여 주어야 한다.

이 작업을 앞 장까지 배운 함수만으로 하면 곧 문제가 생긴다. strlen('소금빵')은 3이 아니라 9를 돌려주고, substr로 글자를 자르면 한글이 깨진다. 이 장은 그 이유부터 설명한다.

바이트, 글자, 너비: mb_* 함수와 UTF-8

PHP 문자열은 바이트 열이다

PHP의 문자열은 글자의 나열이 아니라 바이트(byte, 컴퓨터가 저장하는 가장 작은 단위) 의 나열이다. 영문자와 숫자는 1바이트이므로 문제가 눈에 띄지 않는다. 그러나 오늘날 웹에서 쓰는 문자 인코딩(글자를 바이트로 바꾸는 규칙)인 UTF-8에서는 한글 한 글자가 3바이트를 차지한다. 그래서 strlen, substr, strtoupper처럼 이름에 mb가 없는 함수는 글자를 세거나 자르는 것이 아니라 바이트를 세거나 자른다.

UTF-8에서 한글 한 글자는 3바이트라서 strlen은 9, mb_strlen은 3을 돌려준다.

mb_* 함수는 mbstring 확장(여러 바이트로 된 글자를 다루는 내장 확장)이 제공한다. 이름 앞의 mb는 multibyte의 줄임말이다. 대부분의 PHP 설치본에 들어 있으며, php -m을 실행해 목록에 mbstring이 있는지 확인하면 된다. 이 확장은 별도 설정이 없으면 UTF-8을 기준으로 동작한다. 소스 파일도 UTF-8로 저장해야 한다.

세 가지 길이

한글 문자열에는 서로 다른 길이가 세 가지 있다.

  • 바이트 수: strlen. 저장 공간이나 전송 크기를 볼 때 쓴다.
  • 글자 수: mb_strlen. 이름이 2자에서 10자 사이인지 확인할 때 쓴다.
  • 표시 너비: mb_strwidth. 한글처럼 넓게 그려지는 글자를 2칸, 영문·숫자를 1칸으로 센다. 글자 수가 같아도 화면에서 차지하는 폭은 다르므로, 정해진 폭에 맞춰 자를 때 쓴다.

글자 단위로 자르려면 mb_substr을 쓴다. 시작 위치에 음수를 주면 끝에서부터 센다. 폭에 맞춰 자르고 끝에 표시를 붙이려면 mb_strimwidth를 쓴다. 이 함수의 폭에는 끝에 붙일 표시(...)의 폭도 포함된다. 글자를 낱개 배열로 나누는 함수는 mb_str_split이고, 대소문자 변환은 mb_strtoupper와 mb_strtolower다. 예를 들어 mb_strtoupper('éclair')는 ÉCLAIR를 돌려주지만 strtoupper는 바이트 단위라서 é를 바꾸지 못한다. 글자 수를 맞춰 빈칸을 채우는 mb_str_pad는 PHP 8.3부터 제공되므로 8.4 이상인 이 책의 환경에서 쓸 수 있다.

깨진 바이트를 걸러 내기

브라우저가 UTF-8이 아닌 값을 보내거나 누군가 일부러 이상한 바이트를 보낼 수 있다. mb_check_encoding($text, 'UTF-8')은 문자열이 올바른 UTF-8이면 true를 돌려준다. 사용자 입력은 다른 처리를 하기 전에 이 검사를 먼저 하는 것이 좋다. 뒤에서 볼 preg_* 함수도 깨진 바이트를 만나면 실패한다.

또 하나 알아 둘 것이 있다. 앞 장까지 쓴 trim은 ASCII 공백, 탭, 줄바꿈만 지운다. 한글 입력기에서 나오는 전각 공백(U+3000)은 지우지 못한다. 이 공백은 아래의 정규식으로 처리한다.

서식 만들기: sprintf와 number_format

sprintf는 자리표시자가 들어 있는 서식 문자열에 값을 끼워 문자열을 만든다. printf는 같은 서식으로 만든 문자열을 바로 출력한다. 자리표시자는 %로 시작한다.

자주 쓰는 sprintf 자리표시자
자리표시자뜻예결과
%s문자열sprintf('%s', '빵')빵
%d정수sprintf('%d', 42)42
%04d0으로 채운 4칸 정수sprintf('%04d', 7)0007
%.2f소수점 아래 2자리sprintf('%.2f', 3.14159)3.14
%-6s왼쪽 정렬 6칸(바이트 기준)sprintf('[%-6s]', 'ab')[ab ]
%%퍼센트 기호 자체sprintf('%d%%', 5)5%

칸 수를 채우는 기능은 바이트를 기준으로 한다는 점을 기억해 두자. 한글이 섞인 표를 맞출 때 이 점이 문제가 되며, 아래 「실무에서 자주 틀리는 것」에서 다시 본다.

number_format은 숫자를 사람이 읽기 좋게 바꾼다. 인수를 하나만 주면 소수점 이하를 반올림해서 버리고 세 자리마다 쉼표를 넣는다. number_format(12500)은 '12,500'이고, number_format(1234567.891, 2)는 '1,234,567.89'다. 결과가 문자열이라는 점에 주의한다. 쉼표가 들어 있으므로 이 값으로 다시 계산하면 안 된다. 계산은 정수 그대로 하고, 화면에 내보내는 마지막 순간에만 number_format을 부른다.

패턴으로 찾고 바꾸기: 정규식

정규식(regular expression)은 문자열이 어떤 모양인지를 적는 작은 언어다. PHP에서는 preg_로 시작하는 함수들이 정규식을 쓴다. 패턴은 문자열로 넘기며, 앞뒤를 같은 기호(구분자)로 감싼다. 이 책에서는 /를 쓴다. 닫는 구분자 뒤에 붙는 글자는 옵션(플래그)이다.

한글을 다룰 때 가장 중요한 옵션은 u다. 이 옵션이 없으면 패턴과 대상 문자열을 바이트 단위로 읽고, 있으면 UTF-8 글자 단위로 읽는다. 한글이 들어갈 가능성이 있는 패턴에는 항상 u를 붙이는 습관을 들이자.

이 장에서 쓰는 정규식 요소
요소뜻예맞는 문자열
\d숫자 한 글자/\d\d/07
\s공백류 한 글자/a\sb/a b
\p{Hangul}한글 한 글자(u 필요)/\p{Hangul}+/u소금빵
+, {3,4}1번 이상, 3~4번 반복/\d{3,4}/1234
^, $문자열의 처음과 끝/^\d+$/2026
(?<이름>…)이름 붙은 묶음/(?<qty>\d+)/qty에 숫자 저장

검사: preg_match

preg_match($패턴, $문자열, $결과)는 맞는 곳이 있으면 1, 없으면 0, 오류가 나면 false를 돌려준다. 묶음에 이름을 붙이면 $결과['이름']으로 꺼낼 수 있다. 전체가 패턴과 맞는지 볼 때는 ^와 $로 양끝을 고정해야 한다. 고정하지 않으면 문자열 중간의 일부만 맞아도 통과한다. 결과를 비교할 때는 === 1로 써서 오류(false)와 불일치(0)를 함께 걸러 낸다.

여러 곳을 한꺼번에 찾을 때는 preg_match_all을 쓴다. 옵션에 PREG_SET_ORDER를 주면 맞은 곳마다 하나의 배열이 만들어져 foreach로 돌리기 편하다.

치환: preg_replace

preg_replace($패턴, $바꿀 문자열, $대상)은 맞은 곳을 모두 바꾼다. 패턴의 소괄호로 묶은 부분은 바꿀 문자열 안에서 $1, $2로 다시 쓸 수 있다. 이 함수는 대상이 깨진 UTF-8이면 null을 돌려준다. 반환값이 문자열이라고 믿고 바로 쓰면 안 되는 이유가 여기에 있다.

입력 정리의 순서

사용자 입력을 받으면 아래 순서로 처리한다. 앞 단계를 통과하지 못한 값은 뒤 단계로 보내지 않는다.

입력은 UTF-8 검사, 공백 정리, 형식 검사를 차례로 통과해야 하고 어느 단계에서든 실패하면 null로 거부한다.

먼저 UTF-8 검사를 하고, 다음에 공백을 정리하고, 마지막에 길이와 형식을 검사한다. 이 순서를 바꾸면 안 되는 이유가 있다. 공백을 정리하기 전에 길이를 재면 앞뒤 공백이 글자 수에 들어가고, 깨진 바이트를 두고 정규식을 돌리면 null이 튀어나온다. 이 책에서는 검사에 실패한 값을 null로 돌려주는 방식을 쓴다. 호출한 쪽은 null인지 확인해 오류 메시지를 정한다.

완성 코드

아래 파일 하나를 main.php로 저장한다. 폼에서 받은 값을 흉내 낸 배열을 $input에 직접 적어 두었다. 요청을 실제로 받는 방법은 뒤에서 요청과 응답을 다루는 장에서 배운다.

main.php

<?php
declare(strict_types=1);

const MENU = [
    '소금빵' => 3200,
    '바게트' => 4500,
    '단팥빵' => 2800,
];
const DISCOUNT_FROM = 20000;
const DISCOUNT_PERCENT = 5;

function cleanText(string $raw): string
{
    $text = preg_replace('/[\s\p{Z}]+/u', ' ', $raw);
    if ($text === null) {
        return '';
    }
    return trim($text);
}

function normalizeName(string $raw): ?string
{
    if (!mb_check_encoding($raw, 'UTF-8')) {
        return null;
    }
    $name = cleanText($raw);
    $length = mb_strlen($name);
    if ($length < 2 || $length > 10) {
        return null;
    }
    if (preg_match('/^[\p{Hangul}A-Za-z ]+$/u', $name) !== 1) {
        return null;
    }
    return $name;
}

function normalizePhone(string $raw): ?string
{
    $digits = preg_replace('/\D+/', '', $raw) ?? '';
    $pattern = '/^(?<a>01[016789])(?<b>\d{3,4})(?<c>\d{4})$/';
    if (preg_match($pattern, $digits, $m) !== 1) {
        return null;
    }
    return sprintf('%s-%s-%s', $m['a'], $m['b'], $m['c']);
}

function maskName(string $name): string
{
    $length = mb_strlen($name);
    if ($length < 2) {
        return $name;
    }
    $first = mb_substr($name, 0, 1);
    if ($length === 2) {
        return $first . '*';
    }
    return $first . str_repeat('*', $length - 2) . mb_substr($name, -1);
}

function maskPhone(string $phone): string
{
    return preg_replace('/(\d{3})-\d{3,4}-(\d{4})/', '$1-****-$2', $phone) ?? $phone;
}

function parseItems(string $text): array
{
    $count = preg_match_all(
        '/(?<item>\p{Hangul}+)\s*[x*]\s*(?<qty>\d+)/u',
        $text,
        $matches,
        PREG_SET_ORDER
    );
    $items = [];
    if ($count === false || $count === 0) {
        return $items;
    }
    foreach ($matches as $match) {
        $item = $match['item'];
        $items[$item] = ($items[$item] ?? 0) + (int) $match['qty'];
    }
    return $items;
}

function buildReceipt(string $orderNo, string $name, string $phone, array $items): string
{
    $lines = [
        sprintf('주문번호 %s', $orderNo),
        sprintf('고객 %s (%s)', maskName($name), maskPhone($phone)),
    ];
    $total = 0;
    foreach ($items as $item => $qty) {
        if (!isset(MENU[$item])) {
            $lines[] = sprintf('- %s: 메뉴에 없어 제외', $item);
            continue;
        }
        $subtotal = MENU[$item] * $qty;
        $total += $subtotal;
        $lines[] = sprintf('- %s x %d = %s원', $item, $qty, number_format($subtotal));
    }
    $discount = $total >= DISCOUNT_FROM ? intdiv($total * DISCOUNT_PERCENT, 100) : 0;
    $lines[] = sprintf('합계 %s원', number_format($total));
    if ($discount > 0) {
        $lines[] = sprintf('할인 %.1f%% -%s원', DISCOUNT_PERCENT, number_format($discount));
    }
    $lines[] = sprintf('결제 %s원', number_format($total - $discount));
    return implode("\n", $lines);
}

function showLengths(string $text): void
{
    printf("문자열: %s\n", $text);
    printf("strlen      : %d\n", strlen($text));
    printf("mb_strlen   : %d\n", mb_strlen($text));
    printf("mb_strwidth : %d\n", mb_strwidth($text));
    printf("글자 나누기 : %s\n", implode(' / ', mb_str_split($text)));
}

$input = [
    'name' => "\u{3000}김민수 \t",
    'phone' => '010 1234-5678',
    'items' => '소금빵 x4, 바게트*2, 마카롱 x 2',
    'memo' => "  내일   오전\n10시에   찾으러 갑니다  ",
];

echo "== 글자 세기 ==\n";
showLengths('소금빵');

echo "\n== 이름 검사 ==\n";
$names = [
    '한 글자' => '김',
    '숫자 섞임' => 'Kim123',
    '깨진 바이트' => "\xEC\x82",
    '영문' => ' Anna  Lee ',
];
foreach ($names as $label => $raw) {
    printf("%s: %s\n", $label, normalizeName($raw) ?? '거부');
}

echo "\n== 전화번호 검사 ==\n";
foreach (['010.999.8888', '02-123-4567', '010-12'] as $raw) {
    printf("%s: %s\n", $raw, normalizePhone($raw) ?? '거부');
}

echo "\n== 주문 영수증 ==\n";
$name = normalizeName($input['name']);
$phone = normalizePhone($input['phone']);
if ($name === null || $phone === null) {
    echo "입력을 확인해 주세요.\n";
    exit(1);
}
$orderNo = sprintf('BK-%s-%04d', '20260930', 7);
echo buildReceipt($orderNo, $name, $phone, parseItems($input['items'])), "\n";
printf("메모: %s\n", mb_strimwidth(cleanText($input['memo']), 0, 25, '...'));

줄별 해설

상수와 cleanText

MENU는 품목 이름을 키로, 가격을 값으로 하는 연관 배열이다. 세 상수는 파일 맨 위에 두어 가격 정책이 바뀔 때 한 곳만 고치게 했다.

cleanText는 패턴 /[\s\p{Z}]+/u로 공백류가 하나 이상 이어진 부분을 찾아 공백 하나로 바꾼다. \s는 탭과 줄바꿈을, \p{Z}는 전각 공백을 포함한 모든 구분 공백을 잡는다. 앞뒤에 남은 공백 한 칸은 마지막의 trim이 지운다. preg_replace가 null을 돌려주는 경우(깨진 UTF-8)를 if로 처리해서 함수가 언제나 문자열을 돌려주게 했다.

normalizeName과 normalizePhone

normalizeName은 앞서 정리한 순서를 그대로 따른다. UTF-8을 검사하고, cleanText로 정리하고, mb_strlen으로 2~10자인지 보고, 마지막으로 한글·영문·공백만 있는지 preg_match로 본다. 패턴 [\p{Hangul}A-Za-z ]의 끝에 있는 공백 한 칸도 허용 문자다. !== 1은 오류와 불일치를 모두 거부로 처리한다.

normalizePhone은 숫자가 아닌 것(\D)을 모두 지워 숫자만 남긴다. 그다음 패턴이 010 같은 앞자리 세 개, 가운데 3~4자리, 끝 4자리를 묶음 a, b, c로 받는다. 가운데의 {3,4}는 처음에 4자리를 가져가 보고, 뒤가 모자라면 3자리로 물러난다. 그래서 11자리와 10자리 번호를 모두 받을 수 있다. 통과하면 sprintf로 하이픈을 넣어 돌려준다.

마스킹

maskName은 글자 수에 따라 갈린다. 한 글자면 그대로 두고, 두 글자면 첫 글자 뒤에 *를 붙이고, 세 글자 이상이면 첫 글자와 마지막 글자만 남긴다. 마지막 글자는 mb_substr($name, -1)로 꺼낸다. maskPhone은 preg_replace의 $1, $2로 가운데 부분을 별표로 바꾼다. 실패했을 때는 ??로 원래 값을 돌려주게 했는데, 이 함수는 정규화된 전화번호만 받으므로 실제로 그 경우는 생기지 않는다.

parseItems

preg_match_all은 한글 품목명, 공백, x 또는 *, 공백, 수량 순서로 이어진 곳을 모두 찾는다. [x*]는 대괄호 안의 글자 중 하나라는 뜻이며, 안에서는 *가 특별한 의미를 잃는다. 같은 품목이 두 번 나오면 수량을 더한다. $items[$item] ?? 0은 키가 아직 없을 때 0으로 시작하게 한다. 반환값이 false이거나 0이면 빈 배열을 돌려준다.

buildReceipt

메뉴에 없는 품목은 isset(MENU[$item])로 걸러 내고 continue로 다음 품목으로 넘어간다. 금액은 끝까지 정수로 계산하고 number_format은 문자열로 만드는 순간에만 쓴다. 할인은 합계가 2만 원 이상일 때 5퍼센트이며, intdiv로 나머지를 버린 정수를 얻는다. %.1f%%는 5를 5.0으로 찍고 뒤에 퍼센트 기호를 붙인다. 마지막에 implode로 줄들을 줄바꿈으로 이어 붙인다.

showLengths와 실행부

showLengths는 같은 문자열의 바이트 수, 글자 수, 표시 너비, 글자 나누기 결과를 나란히 보여 준다. 실행부의 $names 배열은 라벨과 입력값의 짝이다. "\xEC\x82"는 한글 한 글자(3바이트) 중 앞 2바이트만 있는 깨진 문자열이라서 UTF-8 검사에서 걸린다. 마지막 줄의 mb_strimwidth는 표시 너비 25에 맞추되 끝의 ...(너비 3)까지 포함하므로 내용은 너비 22 이내로 잘린다.

실행 결과

$ php main.php
== 글자 세기 ==
문자열: 소금빵
strlen      : 9
mb_strlen   : 3
mb_strwidth : 6
글자 나누기 : 소 / 금 / 빵

== 이름 검사 ==
한 글자: 거부
숫자 섞임: 거부
깨진 바이트: 거부
영문: Anna Lee

== 전화번호 검사 ==
010.999.8888: 010-999-8888
02-123-4567: 거부
010-12: 거부

== 주문 영수증 ==
주문번호 BK-20260930-0007
고객 김*수 (010-****-5678)
- 소금빵 x 4 = 12,800원
- 바게트 x 2 = 9,000원
- 마카롱: 메뉴에 없어 제외
합계 21,800원
할인 5.0% -1,090원
결제 20,710원
메모: 내일 오전 10시에 찾으...

실무에서 자주 틀리는 것

substr로 한글을 자른다

틀린 코드는 바이트 4개를 잘라 낸다. 소 한 글자(3바이트)와 금의 첫 바이트가 남아 깨진 글자가 출력된다.

echo substr('소금빵', 0, 4);   // 소 뒤에 깨진 바이트가 붙는다

글자 단위 함수로 고친다.

echo mb_substr('소금빵', 0, 2);   // 소금

sprintf로 한글 칸을 맞춘다

%-8s는 8바이트가 되도록 공백을 채운다. 빵은 3바이트이므로 공백이 5칸만 붙는다. 영문 ab는 6칸이 붙어서 줄이 맞지 않는다.

echo sprintf('[%-8s]', '빵');   // [빵     ]  공백 5칸

화면 너비를 기준으로 직접 채운다. 빵의 표시 너비는 2이므로 공백 6칸이 붙는다.

$text = '빵';
echo '[' . $text . str_repeat(' ', max(0, 8 - mb_strwidth($text))) . ']';   // [빵      ]

패턴에 u 플래그를 빼먹는다

플래그가 없으면 점(.)이 글자가 아니라 바이트 하나에 대응한다. 빵은 3바이트이므로 글자 하나를 기대한 패턴이 맞지 않는다.

var_dump(preg_match('/^.$/', '빵'));    // int(0)
var_dump(preg_match('/^.$/u', '빵'));   // int(1)

preg_replace의 null을 그대로 넘긴다

대상이 깨진 UTF-8이면 preg_replace는 null을 돌려준다. declare(strict_types=1)가 켜진 파일에서 이 값을 trim에 넘기면 TypeError로 프로그램이 멈춘다.

$raw = "\xEC\x82";
echo trim(preg_replace('/\s+/u', ' ', $raw));   // TypeError: trim() 인수는 string 이어야 한다

먼저 UTF-8을 검사하거나 null일 때의 대체값을 정해 둔다.

$raw = "\xEC\x82";
$text = preg_replace('/\s+/u', ' ', $raw) ?? '';
echo trim($text);   // 빈 문자열

한눈에 보기

바이트 단위 함수와 글자 단위 함수의 대응
하는 일바이트 단위글자 단위비고
길이strlenmb_strlen소금빵은 9와 3
화면 너비없음mb_strwidth한글 2, 영문 1
자르기substrmb_substr음수 시작 위치 가능
너비로 자르기없음mb_strimwidth끝 표시의 너비 포함
대문자로strtouppermb_strtoupper바이트판은 ASCII만
낱글자 배열str_splitmb_str_split한글은 mb 사용
칸 채우기str_padmb_str_padPHP 8.3 이상
UTF-8 검사없음mb_check_encoding입력 처리의 첫 단계

서식은 sprintf와 number_format, 검사는 preg_match, 여러 곳 찾기는 preg_match_all, 치환은 preg_replace가 맡는다. 한글이 들어가는 패턴에는 u를 붙이고, 반환값의 false와 null을 확인한다.

연습 문제

  1. 문자열 '바닐라 슈크림빵'의 바이트 수, 글자 수, 표시 너비를 먼저 손으로 계산한 다음 코드로 확인하라.
  2. 주문번호 'BK-20260930-0007'에서 날짜와 순번을 이름 붙은 묶음으로 꺼내는 preg_match 코드를 작성하라. 순번은 정수 7로 바꿔 출력한다.
  3. 금액 1234567을 1,234,567원 꼴로, 할인율 수치 7.5를 7.5% 꼴로 만드는 한 줄씩의 코드를 작성하라.
  4. 메모 '010-1234-5678로 연락 주세요' 안의 전화번호 가운데 네 자리를 ****로 바꾸는 코드를 작성하라.

정답과 해설

  1. 글자는 바닐라(3), 공백(1), 슈크림빵(4)으로 8글자다. 한글 7자는 각각 3바이트이고 공백은 1바이트이므로 바이트 수는 7×3+1=22다. 표시 너비는 한글 7자가 각 2, 공백이 1이므로 15다.
    $s = '바닐라 슈크림빵';
    echo strlen($s), ' ', mb_strlen($s), ' ', mb_strwidth($s), "\n";   // 22 8 15
  2. 날짜는 숫자 8자리, 순번은 4자리로 잡고 양끝을 고정한다. 순번 '0007'은 문자열이므로 (int)로 바꿔야 7이 된다.
    $no = 'BK-20260930-0007';
    if (preg_match('/^BK-(?<date>\d{8})-(?<seq>\d{4})$/', $no, $m) === 1) {
        echo $m['date'], ' ', (int) $m['seq'], "\n";   // 20260930 7
    }
  3. 쉼표는 number_format, 소수 한 자리와 퍼센트 기호는 sprintf로 만든다. 퍼센트 기호를 찍으려면 %%로 써야 한다.
    echo number_format(1234567) . '원';    // 1,234,567원
    echo sprintf('%.1f%%', 7.5);           // 7.5%
  4. 앞 세 자리와 끝 네 자리를 묶음으로 잡아 $1, $2로 되살린다. 가운데는 묶지 않았으므로 결과에서 사라지고 별표가 들어간다.
    $memo = '010-1234-5678로 연락 주세요';
    echo preg_replace('/(\d{3})-\d{4}-(\d{4})/', '$1-****-$2', $memo);
    // 010-****-5678로 연락 주세요

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.