The Pursuit of Happyness

오늘은 SAP 쪽에서 웹 클라이언트 개발에 많이 사용되는 OpenUI5 프레임워크에 대해서 설명해 보겠습니다.


OpenUI5 는 http://openui5.org/ 홈페이지에서 Apache 2.0 라이센스로 무료로 배포하고 있습니다.


기업용 소프트웨어를 오랜동안 서비스해서 인지, 프레임워크가 상당히 깔끔하고 안정된 느낌입니다.



웹사이트에서 소개된 Key Features의 제목은 다음과 같습니다.


1. 엔터프라이즈  웹 툴킷


2. 파워풀한 개발 컨셉


3. 풍부한 UI 컨트롤을 제공


4. 일관성 있는 사용자 경험


5. 무료로 배포되는 오픈소스


6. 웹 브라우저 및 휴대용 기기에서 같이 사용가능한 반응형




Get Started 섹션에서는 OpenUI5를 사용하는 기본적인 방법이 소개되어 있습니다. OpenUI5를 사용법은 다음과 같습니다.


1. 웹페이지가 로딩될 때 UI5 라이브러리가 로딩될 수 있게 스크립트를 추가합니다. 


먼저 소스코드는 다음과 같은데, OpenUI5 라이브러리를 resources 폴더에 미리 준비해 두거나 (다운로드 받아서 압축 해재) sap cdn 링크로 걸어 주면 됩니다.


<!DOCTYPE html>
<html>
<head>
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<title>Hello World</title>

<script id='sap-ui-bootstrap'
    src='resources/sap-ui-core.js'
    data-sap-ui-theme='sap_bluecrystal'
    data-sap-ui-libs='sap.m'
    data-sap-ui-compatVersion='edge'
    data-sap-ui-preload='async'>
</script>

<script>
     sap.ui.getCore().attachInit(function () {
         jQuery("#content").html("Hello World - UI5 is ready");
     });
</script>

</head>
<body class='sapUiBody'>
    <div id='content'></div>
</body>
</html>


OpenUI5 에 jquery.sap 라이브러리가 포함되어 있어서 별도로 jQuery 라이브러리를 추가하지 않아도 jQuery 기능을 사용할 수 있습니다. (물론 포함되어 있는 라이브러리 버전에 따라 지원되는 내용이 다를 수 있습니다.)


AngularJS 등의 프레임워크를 사용해 보신 분들은 쉽게 이해하실 수 있는 코드라 생각됩니다. body에 div 엘리먼트를 준비해 두고, 자바스크립트를 이용하여 페이지가 로딩될 때, div 에 "Hello World - UI5 is ready" 라는 텍스트를 넣어주게 됩니다.


2. OpenUI5 컨트롤을 이용합니다.


<!DOCTYPE html>
<html>
<head>
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<title>Hello World</title>

<script id='sap-ui-bootstrap'
    src='resources/sap-ui-core.js'
    data-sap-ui-theme='sap_bluecrystal'
    data-sap-ui-libs='sap.m'
    data-sap-ui-compatVersion='edge'
    data-sap-ui-preload='async'>
</script>

<script>
    sap.ui.getCore().attachInit(function () {
        new sap.m.Text({
            text: "Hello World"
        }).placeAt("content");
    });
</script>

</head>
<body class='sapUiBody'>
    <div id='content'></div>
</body>
</html>


위에서는 jQuery를 이용하여 div 엘리먼트에 값을 넣어 주었다면, 이번에는 OpenUI5에 있는 UI 컨트롤 객체(여기서는 sap.m.Text)를 생성해서 컨트롤 객체의 메쏘드 placeAt을 이용하여 해당 div 엘리먼트에 컨트롤 객체를 설정해 주었습니다.


3. XML 뷰 (코드는 http://openui5.org/getstarted.html#step3 에서 확인하세요)

웹 애플리케이션의 View를 XML 로 만들어서 별도의 파일로 생성합니다. 나중에 나오지만 OpenUI5는 XML 이외에도 Javascript 나 HTML 형태의 View도 지원하기 때문에 필요한 형태의 View를 사용하시면 됩니다. 뷰 파일명은 *.view.xml 형식의 컨벤션을 사용합니다. 웹페이지의 설명에서는 App.view.xml 이라는 이름을 사용했습니다.


4. 컨트롤러 (코드는 http://openui5.org/getstarted.html#step4 에서 확인하세요)

뷰와 마찬가지로 컨트롤러도 별도의 파일로 생성을 합니다. 

컨트롤러에 대한 컨벤션입니다.

- 컨트롤러 이름의 첫글자는 대문자를 사용한다.

- 연관된 뷰와 1:1 로 매칭시키며, 같은 이름을 사용한다.

- 이벤트 핸들러는 "on"으로 시작한다.

- 컨트롤러 파일명은 *.controller.js 로 끝난다.


5. 데이터 바인딩 (코드는 http://openui5.org/getstarted.html#step5 에서 확인하세요)

UI 컨트롤 객체에 데이터 모델을 연결합니다.

예제에서는 컨트롤러에서 JSON 모델을 생성하고, 뷰를 가지고 와서 생성한 JSON 모델을 세팅합니다.

뷰에서는 Input (sap.m.Input) 객체를 생성하면서 value 를 모델의 /recipient/name 값에 연결시킵니다.

또한 description 도 모델의 /recipient/name 과 연결합니다.

이렇게 연결을 해 두면 사용자가 Input 의 값을 변경하면, 연결된 모델의 /recipient/name 값이 같이 변경되며, 반대로 컨트롤러에서 모델의 /recipient/name 값을 변경하면, Input 값도 같이 변경이 됩니다. (value 와 description)

XML 뷰에서의 데이터 바인딩 문법은 다음과 같습니다.

- 중괄호"{ ... }" 는 데이터 바인딩이 사용되었음을 나타냄

- "/recipient/name" 은 해당 모델에서의 경로를 선언함




Comment +0

Linux 콘솔에서는 기본적으로 하나의 foreground job 을 수행할 수 있는데, 여러 개의 작업을 동시에 실행할 때에는 screen 명령을 이용해서 가상의 콘솔을 이용할 수 있다.


1. 기본 사용법


> screen 



2. 스크린 추가하기


^A^C



3. 스크린 닫기


exit : 현재 스크린 닫기

^D : 현재 스크린 닫기

^A\ : 전체 스크린 종료 및 screen 명령 종료



4. 스크린 전환하기


^A^P : 이전 번호의 스크린으로

^A^N : 다음 번호의 스크린으로

^A0, ^A1, ... ^A9 : 해당 번호의 스크린으로 이동

^A' : 이동할 스크린 번호 입력해서 이동

^A^A : 바로 전에 사용한 스크린으로 이동



5. 현재 스크린 확인


^AN : 현재 스크린 번호 확인

^A^W : 전체 스크린 리스트 확인





Comment +0

Linux에서는 여러 개의 프로그램을 동시에 실행시킬 수가 있는데, 


하나의 콘솔창에서는 보통 하나의 Job만 foreground에서 실행이 가능하고, 나머지 Job은 Background에서 실행이 된다.


이 포스트에서는 Foreground / Background Job을 관리하는 방법에 대해서 알아본다.


1. Job


현재 콘솔에서 실행되고 있는 Job들의 리스트를 확인하는 명령어


> jobs 



2. 백그라운드에서 프로그램을 실행하는 방법 


명령 끝에 '&' 를 붙여준다. 실행 하게 되면 job id와 프로세스 id 를 보여준다. (아래 예제에서는 job id = 1, process id = 910)


> sleep 1000 &

[1]+ 910



3. 현재 실행중인 명령을 잠시 중단하는 방법


명령어 실행 후 ^Z  (ctrl + z) 를 입력한다.


> sleep 1000 &

^Z
[1]+ Stopped          sleep 1000


Stopped 라고 나오지만 실제로는 종료되지 않은 상태로 남아 있다. (jobs 명령어로 확인이 가능)



4. 잠시 중단한 명령을 백그라운드로 보내는 방법


bg %[id]


> bg %1

[1]+ sleep 1000 &



5. 백그라운드에서 실행중인 job을 foreground로 바꾸는 방법


fg %[id]


> fg %1

sleep 1000



6. 실행 중인 명령을 중단하는 방법


명령어 실행 후 ^C (ctrl + c) 를 입력한다.



참고로 백그라운드에서 작업 중인 명령을 종료하기 위해서는 해당 job을 foreground로 넘긴 후 ^C 를 입력하면 된다.


혹은 kill 명령어를 이용해서 해당 process 를 종료 하면 된다. (process id 는 ps 명령어로 검색한다.)


> kill 910







Comment +0

테스팅


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0



- 테스트를 하는 이유

애플리케이션이 우리의 기대에 따라 지속적으로 작동하도록 보장하기 위해서!


- 테스트 시점

-- 개발 전

개발 전에 테스팅 도구를 설치하고 구성해야 한다.

-- 개발 도중

각 부분을 만들 때마다 테스트를 작성하고 실행한다. 새로운 클래스를 추가하면 그 즉시 테스트를 만들어 실행한다.

-- 개발 후

애플리케이션 출시 후 버그를 발견하면 버그 픽스를 만들어야 하며, 올바른 동작을 보장하기 위해 새로운 테스트도 함께 작성한다.

코드 수정시 그에 따른 영향을 받는 테스트 역시 수정해야 한다.


- 테스트 대상

애플리케이션을 구성하는 가장 작은 조각들 - 유닛 테스트 (미시적 관점)

전체 애플리케이션 - 기능 테스트 (거시적 관점)


- 테스트 방법

-- 단위 테스트

큰 애플리케이션에 포함된 클래스, 메써드, 함수를 고립시킨 상태로 검증하는 테스트 방식


-- 테스트 주도 개발 (TDD)

테스트 작성이 애플리케이션 코드 작성보다 선행

의도적인 실패를 통해 애플리케이션의 행동 방식을 묘사하고, 애플리케이션의 기능이 갖춰지고 나서야 성공적으로 수행된다.

목표 지향적이며 무엇을 만들어야 할지, 결과물은 어떻게 작동해야 할지를 사전에 인지할 수 있게 도와준다.


-- 행위 주도 개발 (BDD)

애플리케이션이 행동하는 방식을 이야기처럼 묘사하는 개발 방법론.

--- 스펙 BDD

인간 친화적인 언어로 애플리케이션 구현을 묘사하는 단위 테스트 유형

--- 스토리 BDD

하위 수준 구현 보다는 상위 수준 행동에 관심이 있다.


ex) PDF 보고서를 작성하고 이메일로 발송하는 애플리케이션 제작

---- 스펙 BDD : PDF 생성 클래스 메서드가 매개변수를 입력받아 올바르게 PDF 파일을 생성하는 지 검증

---- 스토리 BDD : 프로젝트 관리자 관점에서 PDF가 생성되어지는지와 email이 보내지는 지 같은 포괄적인 애플리케이션 동작을 검증

=> 둘 간의 차이는 테스트 영역이라고 생각하면 됨


- PHP 유닛

PHP 유닛 테스트 => 테스트 케이스 => 테스트 스위트

테스트 러너를 통해 테스트 스위트를 실행한다.

테스트 케이스는 PHPUnit_Framework_TestCase 클래스를 확장한 단일 PHP 클래스

컴포저를 이용하여 설치할 수 있다.


> composer require --dev phpunit/phpunit 




Comment +0

서버 설정 2


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0


- PHP-FRM

PHP 프로세스 풀을 관리하는 소프트웨어

PHP 프로세스는 엔진엑스 같은 웹 서버로부터 받은 요청을 처리


repository를 추가하고 패키지 매니저를 이용하여 설치한다.


 # 우분투

> sudo apt-get install python-software-properties

> sudo add-apt-repository ppa:ondrej/php5-5.6

> sudo apt-get update

> sudo apt-get install php5-fpm php5-cli php5-curl php5-gd php5-json php5-mcrypt php5-mysqlnd


# CentOS

> sudo rpm -Uvh http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-5.noarch.rpm

> sudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

> sudo yum -y --enablerepo=epel, remi, remi-php56 install php-fpm php-cli php-gd php-mbstring php-mcrypt php-mysqlnd php-opcache php-pdo php-devel


- 엔진엑스

엔진엑스는 아파치와 유사한 웹 서버지만 더 적은 시스템 메모리를 사용한다.


-- 설치

CetnOS 의 경우 바로 앞에서 설치한 EPEL 이 추가되어 있다고 가정


 # 우분투

> sudo add-apt-repository ppa:nginx/stable

> sudo apt-get update

> sudo apt-get install nginx


# CentOS

> sudo yum install nginx

> sudo systemctl enable nginx

> sudo systemctl start nginx


-- 가상 호스트

우분투의 경우 /etc/nginx/sites-available/example.conf 

CentOS 의 경우 /etc/nginx/conf.d/example.conf 

설정을 변경한다.


Comment +0

서버 설정 1


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0


1. 최초 로그인

ssh 명령어를 실행해 서버에 접속한다. 처음에는 호스트를 확인하는 메시지가 표시되며 yes를 입력하고 암호를 입력하면 로그인이 완료 된다.


2. 소프트웨어 업데이트

운영체제를 최신으로 업데이트 한다.


# 우분투

>  apt-get update

> apt-get upgrade


# CentOS

> yum update


3. 사용자 계정 추가

루트 사용자의 권한을 제한 하기 위해 deploy 라는 이름의 계정을 생성해서 사용해 보자. 

우분투의 경우 adduser 를 실행하면 바로 사용자 암호를 설정하는 화면으로 넘어가지만 CentOS 의 경우 passwd 명령어로 비밀번호 설정을 별도로 해줘야 한다. deploy 유저에게 sudo 권한을 주기위해서 그룹을 설정해 주는데, 우분투에서는 sudo 그룹으로 CentOS에서는 wheel 그룹에 추가해 준다.


# 우분투

> adduser deploy

> usermod -a -G sudo deploy


# CentOS

> adduser deploy

> passwd deploy

> usermod -a -G wheel deploy


4. SSH 키 쌍 인증

ssh 서버 접속시 비밀번호 인증 대신 SSH 키 쌍 인증을 사용해서 서버 보안을 강화한다.


1). 접속할 로컬 컴퓨터 (클라이언트 PC) 에서 ssh-keygen 명령어를 이용해서 개인키 (~/.ssh/id_rsa), 공개키 (~/.ssh/id_rsa.pub)를 생성한다.

개인키는 로컬 컴퓨터에 두고 유출되지 않도록 보호해야 하며, 공개 키는 접속할 ssh 서버로 scp 등을 이용하여 복사한다.


> ssh-keygen


2). 접속할 로컬 컴퓨터에서 scp ~/.ssh/id_rsa.pub deploy@server_address: 명령어를 입력한다.

마지막에 ":" 콜론을 붙이면 deploy의 홈 디렉터리에 업로드 된다.


> scp ~/.ssh/id_rsa.pub deploy@server_address:


3). ssh 서버에서 deploy 홈 디렉터리에서 mkdir ~/.ssh  명령어를 이용하여 ~/.ssh 디렉터리를 생성한다.  


> mkdir ~/.ssh


4). ssh 서버에서 touch ~/.ssh/authorized_keys 명령어를 이용해서 authorized_keys 파일을 생성한다.


> touch ~/.ssh/authorized_keys


5). ssh 서버에서 cat ~/id_rsa.pub >> ~/.ssh/authorized_keys 명령어를 이용해서 공개키를 등록한다.


> cat ~/id_rsa.pub >> ~/.ssh/authorized_keys


6). ssh 서버에서 deploy 사용자만 ~/.ssh 디렉터리에 접근해서 authorized_keys 파일을 읽을 수 있도록 권한을 변경한다.


> chown -R deploy:deploy ~/.ssh

chmod 700 ~/.ssh

chmod 600 ~/.ssh/authorized_keys


여기까지 작업이 되었으면, 개인 키가 있는 로컬 컴퓨터에서는 비밀번호 없이 ssh로 원격 서버에 접속할 수 있다.


5. 비밀번호 인증 및 루트 로그인 비활성화

1) ssh 서버에서 /etc/ssh/sshd_config 파일을 편집하여 PasswordAuthentication과 PermitRootLogin 설정값을 찾아 모두 no로 수정한다. 주석처리 되어 있으면 주석을 풀어준다.


2) 아래 명령으로 ssh 서비스를 재시작한다.


# 우분투

> sudo service ssh restart


# CentOS

> sudo systemctl restart sshd




Comment +0

PHP 모범 사례 4


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0


- 스트림

파일, 네트워크, 데이터 압축처럼 공통적인 기능과 사용 방법을 공유하는 작업들을 일반화하는 수단.


-- 스트림 래퍼

고유한 프로토콜로 읽고 쓸수 있는 스트림 데이터의 유형. 다음과 같은 공통 과정을 거친다.

1. 통신 시작

2. 데이터 읽기

3. 데이터 쓰기

4. 통신 종료


형식

<scheme>://<target>

ex) 

https://www.google.com


생략시 file scheme으로 인식

ex) 

fopen('/etc/hosts', 'rb');

fopen('file:///etc/hosts', 'rb'); // "file://" 이후에 절대 경로 ("/etc/hosts" )를 적어 줘야 한다.


-- php:// 스트림 래퍼

php://stdin - 읽기 전용 PHP 스트림, CLI 에서 파이프를 통해 스크립트로 전달된 정보를 수신할 수 있다.

php://stdout - 현재 출력 버퍼에 데이터를 기록할 수 있다. (쓰기 전용으로 읽거나 탐색은 불가)

php://memory - 시스템 메모리 데이터를 읽고 쓸 수 있다. 

php://temp - php://memory 스트림과 동일하게 작동하지만 가용 메모리가 고갈되면 임시 파일에 기록된다.


-- 스트림 콘텍스트

스트림 콘텍스트를 이용하면 file_get_contents() 함수를 이용해서 HTTP POST 요청을 보낼 수 있다.


ex)


<?php

$requestBody = '{"username":"josh"}';

$context = stream_context_create( array(

    'http' => array(

        'method' => 'POST', 'header' => "Content-Type: application/json;charset=utf-8;\r\n" . "Content-Length: " . mb_strlen($requestBody), 'content' => $requestBody

    )

));

$response = file_get_contents('https://your_test_server.com/users', false, $context);


-- 스트림 필터

스트림을 전송하는 동안 데이터를 여과, 변형, 추가, 제거하기 위해 사용


ex) 마크다운 파일을 스트림으로 열어 메모리로 읽어들이면서 HTML 변환을 하는 작업

ex) 파일을 읽어서 모두 대문자로 변경해서 출력하는 작업


<?php

$handle = fopen('data.txt', 'rb');

stream_filter_append($handle, 'string.toupper');

while(feof($handle) !== true) {

    echo fgets($handle);

}

fclose($handle);





Comment +0

PHP 모범 사례 3


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0


- 데이터베이스

-- PDO 사용 (PHP data objects)

단일한 사용자 인터페이스로 다양한 SQL 데이터베이스와 통신 가능한 PHP 클래스 집합체

데이터베이스 구현을 추상화하고, 이를 이용하여 DBMS 타입에 관계없이 하나의 인터페이스를 통해 쿼리를 작성하고 실행할 수 있다. (DBMS 마다 다른 Query dialect 는 신경써야 한다.)


-- DB 연결 및 DSN

PHP에서 PDO 클래스 인스턴스를 생성하고 PDO 인스턴스로 PHP와 데이터베이스를 연결한다.

PDO 클래스 생성시 DB 연결 정보를 제공하는 문자열 인수를 DSN 이라고 한다.

DSN은 DB 드라이버명과 호스트명 혹은 IP 주소 그리고 포트번호, DB이름, 문자 집합 등이 포함된다.


ex)

<?php

try {

    $pdo = new PDO('mysql:host=127.0.0.1;port=3306;dbname=acme;charset=utf8', 'dbaccount', 'dbpassword');

} catch (PDOException $e) {

    echo 'fail to connect';

    exit;

} 


-- 데이터베이스 인증 정보 보관

인증 정보는 PHP 파일에 직접 넣어서 사용하면 안된다. 웹 문서 루트 외부에 있는 설정 파일로 옮겨서 PHP 파일에서 읽어서 처리한다. .gitignore 파일을 이용해서 버전 관리에서도 제외 시킨다.


-- 준비된 구문 (Statement)

Query Injection 등의 위험을 제거하기 위해서 Statement를 이용한다.


ex)

<?php

$sql = 'SELECT id FROM users WHERE email = :email';

$statement = $pdo->prepare($sql);


$email = filter_input(INPUT_GET, 'email');

$statement->bindValue(':email', $email, PDO:PARAM_STR);

$statement->execute();


while( ($result = $statement->fetch(PDO::FETCH_ASSOC)) !== false ) {

    echo $result['email'];

}



-- 쿼리 결과

PDO::FETCH_ASSOC - 배열 키는 데이터베이스 열 이름

PDO::FETCH_NUM - 배열 키는 데이터베이스 열 순서 번호

PDO::FETCH_BOTH - 배열 키는 데이터베이스 열 이름과 열 순서 번호 모두 사용 가능

PDO::FETCH_OBJ - 데이터베이스 열 이름이 객체 속성명으로 사용되는 객체 반환


-- 트랜젝션

트랜잭션은 더 이상 나눌 수 없는 데이터베이스 명령문 집합

DBMS에 따라서 지원 여부가 결정된다.


ex)

<?php

$pdo->beginTransaction();


// insert or update queries


$pdo->commit();







Comment +0

PHP 모범 사례 2


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0

- 비밀번호 관리

-- DB가 해킹당해도 비밀번호를 알 수 없게 평문이나 복호화가 가능한 비밀번호를 저장하지 말 것

-- 비밀번호에 제한을 두지 말 것

-- 비밀번호를 절대 이메일로 보내지 말 것

-- bcrypt로 해시화 할 것

암호화 NO, 해시화 YES!

해싱 알고리즘 (MD5, SHA1, bcrypt, scrypt ...)

bcrypt - 단점은 느린 속도. 레인보우 테이블 공격을 저지하기 위해 데이터에 솔트를 추가한다.

password_hash() 함수를 사용하면 default로 bcrypt 해싱 알고리즘을 사용한다. (참고 password_verify(), password_needs_rehash())


- 날짜, 시간, 시간대

-- 기본 시간대 지정

php.ini 파일에 date.timezone 항목을 설정한다.

혹은 date_default_timezone_set() 함수를 이용해서 설정한다.


-- DateTime 클래스

-- DateInterval 클래스, DatePeriod 클래스

DateTime 클래스를 조작하기 위해서 사용된다.


ex)


<?php

$datetime = new DateTime();    // create DateTime instance 


$interval = new DateInterval('P2W');    // Period of 2 weeks


$datetime->add($interval);

echo $datetime->format('Y-m-d H:i:s');


$dateInterval = DateInterval::createFromDateString('-1 day');

$datePeriod = new DatePeriod($datetime, $dateInterval, 3);    // 3 periods

foreach ($datePeriod as $date) {

    echo $date->format('Y-m-d') . PHP_EOL;

}



주) 예전에는 strtotime 을 이용해서 date을 변경해 가면서 for 문을 돌렸는데, 조금 더 직관적인 방법이 되었네요..



Comment +0

PHP 모범 사례 1


http://www.hanbit.co.kr/book/look.html?isbn=978-89-6848-225-0


입력값의 위험을 제거하고 데이터의 유효성을 검사하고 출력을 예외 처리하라!


- 입력값 위험 제거

-- 입력 데이터가 애플리케이션 저장소 계층 (DB, CacheDB 등) 에 도달하기 전에 위험을 제거하는 것이 중요

-- HTML

htmlentities() 함수나 HTML purifier 라이브러리를 이용 

-- SQL

준비된 PDO 문을 사용하여 쿼리 인젝션 공격에 대비한다.

-- 사용자 정보

filter_var(), filter_input() 함수를 이용하여 필터링 한다.


- 유효성 검사

-- filter_var() 함수를 이용하여 유효성 검사를 통해 잠재적인 DB 오류를 방지한다.


- 출력 예외 처리

-- 애플리케이션 사용자에 의해 악의적인 코드가 표시되거나 부주의하게 실행되는 것을 막아준다.

htmlentities() 사용, ENT_QUOTES 를 이용해서 따옴표 처리, 적절한 문자 인코딩을 명시 (보통 UTF-8)


Comment +0