Ver2.5 CodeIgniter & PostgreSQL로힙스터 이커머스 만들기: 찐개발자 바이브

CodeIgniter & PostgreSQL로
힙스터 이커머스 만들기: 찐개발자 바이브
가벼움의 미학 코드이그나이터와 튼튼함의 대명사 포스트그레SQL의 환상 콜라보!
너도 이제 풀스택 이커머스 개발자가 될 수 있어. 나만 믿고 따라와!
안녕! 코딩하는 친구들. 오늘은 좀 특별한 조합으로 전자상거래 사이트를 만들어 볼 거야.
세상에는 수많은 PHP 프레임워크와 데이터베이스가 있지만, 오늘은 그중에서도 **CodeIgniter 4(이하 CI4)**와 **PostgreSQL** 조합에 대해 깊게 파고들어 볼까 해.
왜 하필 이 조합이냐고? 🤔
보통 PHP 하면 'LAMP 스택(Linux, Apache, MySQL, PHP)'을 떠올리잖아? 맞아, 그것도 훌륭한 조합이지. 하지만 개발의 세계는 넓고, 우리는 늘 새로운 무기를 장착해야 하는 법!
**코드이그나이터**는 말이야, 라라벨이나 심포니처럼 기능이 아주 많지는 않지만, 그만큼 엄청나게 가볍고 빨라. 학습 곡선도 낮아서 초보자가 접근하기에도 좋고, 필요한 기능만 쏙쏙 골라 쓸 수 있어서 숙련자에게는 자유도를 주지. 마치 커스텀 PC를 조립하는 느낌이랄까? 필요한 부품만 딱딱 꽂아서 최적의 성능을 내는 거지.
그리고 **포스트그레SQL(PostgreSQL)**! 흔히 'Postgres'라고 부르는데, 이 친구는 정말 '믿음직한 국밥' 같은 데이터베이스야. MySQL도 물론 훌륭하지만, Postgres는 몇 가지 강력한 무기를 가지고 있어.
- **강력한 데이터 무결성:** 트랜잭션 처리가 아주 안정적이라서 돈이 오고 가는 이커머스 사이트에서는 절대적으로 중요해. 주문 처리 중에 오류가 나도 데이터가 꼬일 일이 없다는 뜻!
- **고급 데이터 타입:** JSONB, 배열, hstore 같은 데이터 타입을 네이티브로 지원해. 상품의 다양한 옵션이나 속성을 유연하게 저장하고 검색할 때 정말 편리해.
- **확장성:** 사용자가 정의한 함수나 데이터 타입을 만들 수 있어서, 복잡한 비즈니스 로직을 데이터베이스 레벨에서 처리할 수도 있어.
결론적으로, **빠르고 유연한 CI4**와 **안정적이고 강력한 Postgres**의 조합은, 특히 데이터의 정확성과 복잡한 처리가 중요한 전자상거래 사이트 개발에 있어서 환상의 짝꿍이라고 할 수 있어. 자, 이제 이 둘을 가지고 어떻게 우리만의 멋진 온라인 스토어를 만들 수 있는지 차근차근 알아보자고!
개발 환경 세팅: 삽질은 여기서 끝내자!
모든 위대한 여정은 첫걸음부터 시작되지. 우리의 첫걸음은 바로 개발 환경 세팅이야.
여기서 시간을 허비하면 시작도 전에 지치기 마련이니까, 최대한 빠르고 정확하게 끝내보자고.
1. 준비물 체크리스트
우선 우리에게 필요한 무기들을 점검해봐야 해. 아래 항목들이 네 컴퓨터에 설치되어 있는지 확인해줘.
- **PHP:** 당연히 PHP가 있어야겠지? CI4는 PHP 7.4 이상을 요구해. 이왕이면 최신 버전인 8.x 버전을 쓰는 걸 추천할게.
- **Composer:** PHP 의존성 관리 도구야. 이제 Composer 없이는 PHP 개발을 논할 수 없을 정도지. CI4 설치도 이걸로 할 거야.
- **PostgreSQL 서버:** 우리의 데이터를 저장할 데이터베이스 서버. 로컬에 직접 설치해도 되고, Docker를 사용해도 좋아. 편한 방법으로 준비해줘.
- **PHP-pgsql 확장 모듈:** PHP가 PostgreSQL 데이터베이스와 통신할 수 있게 해주는 다리 역할을 하는 모듈이야. 이게 없으면 연결이 안 되니 꼭 확인해야 해! `php.ini` 파일에서 `extension=pgsql` 또는 `extension=php_pgsql.dll` 라인의 주석(`;`)이 제거되어 있는지 확인하면 돼.
2. 코드이그나이터 4 설치하기
자, 이제 본격적으로 CI4를 설치해볼까? 터미널을 열고 프로젝트를 생성하고 싶은 폴더로 이동한 다음, 아래 명령어를 입력해줘.
composer create-project codeigniter4/appstarter my-ecommerce-project
이 명령어는 `my-ecommerce-project`라는 이름의 폴더를 만들고 그 안에 CI4의 기본 구조를 다운로드해 줄 거야. 설치가 끝나면 해당 폴더로 이동하자.
cd my-ecommerce-project
CI4는 자체적으로 개발 서버를 내장하고 있어서 아파치나 Nginx 같은 웹 서버 설정 없이도 바로 실행해볼 수 있어. 정말 편리하지?
php spark serve
이제 웹 브라우저를 열고 `http://localhost:8080`으로 접속해봐. 코드이그나이터 환영 페이지가 보인다면 성공적으로 설치된 거야! 🎉
3. PostgreSQL 데이터베이스 연결하기
프레임워크는 준비됐고, 이제 데이터베이스와 연결할 차례야. CI4의 설정은 정말 간단해.
프로젝트 루트 폴더에 있는 `env` 파일을 복사해서 `.env`라는 이름의 파일을 만들어줘. 이 `.env` 파일이 바로 우리의 환경 설정 파일이야. 앞으로 모든 설정은 여기서 관리할 거야.
이제 `.env` 파일을 열고 데이터베이스 관련 부분을 찾아서 아래처럼 수정해줘.
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
database.default.hostname = localhost
database.default.database = my_ecommerce_db
database.default.username = your_postgres_user
database.default.password = your_postgres_password
database.default.DBDriver = Postgre
database.default.DBPrefix =
database.default.port = 5432
각 항목을 설명해줄게.
- `hostname`: PostgreSQL 서버 주소야. 로컬에 설치했다면 `localhost` 또는 `127.0.0.1`.
- `database`: 우리가 사용할 데이터베이스 이름이야. 미리 `CREATE DATABASE my_ecommerce_db;` 명령어로 만들어둬야 해.
- `username`: PostgreSQL에 접속할 사용자 이름.
- `password`: 해당 사용자의 비밀번호.
- `DBDriver`: **이게 제일 중요해!** 기본값은 `MySQLi`인데, 우리는 Postgres를 쓸 거니까 반드시 `Postgre`로 바꿔줘야 해.
- `port`: PostgreSQL의 기본 포트는 `5432`야. 혹시 다른 포트를 쓴다면 수정해줘.
설정을 마쳤으면, 이제 정말 연결이 잘 되는지 확인해봐야겠지? CI4의 `spark` 명령어는 이런 테스트도 쉽게 할 수 있게 도와줘.
php spark db:table
이 명령어를 실행했을 때, "There are no tables in the current database." 같은 메시지가 나오거나 아무 에러 없이 끝나면 성공적으로 연결된 거야. 만약 "Unable to connect to the database" 같은 에러가 뜬다면, `.env` 파일의 설정값이나 `php-pgsql` 모듈이 제대로 활성화되었는지 다시 한번 꼼꼼히 확인해봐.
이커머스의 심장, 데이터베이스 설계하기
환경 설정이 끝났으니 이제 진짜 설계를 시작할 시간이야. 이커머스 사이트에서 데이터베이스는 사람의 '심장'과도 같아. 모든 상품 정보, 고객 데이터, 주문 기록이 여기에 저장되고 처리되니까.
여기서 설계를 잘못하면 나중에 코드가 복잡해지고 성능이 떨어지는 대참사가 발생할 수 있어. 그러니 신중하게, 하지만 너무 겁먹지는 말고 시작해보자!
1. 핵심 테이블 구상하기
우선 우리 쇼핑몰에 꼭 필요한 핵심 테이블들이 뭐가 있을지 생각해보자.
- **users (회원):** 우리 사이트에 가입한 고객들의 정보를 저장해. 아이디, 암호화된 비밀번호, 이메일, 이름, 주소 등.
- **products (상품):** 판매할 상품들의 정보를 담아. 상품명, 설명, 가격, 재고 수량, 이미지 경로 등.
- **categories (카테고리):** 상품들을 분류하기 위한 테이블이야. '상의', '하의', '신발'처럼. 대분류, 소분류를 위해 부모-자식 관계를 가질 수도 있어.
- **orders (주문):** 고객이 주문한 내역의 마스터 정보. 누가, 언제, 총 얼마를 주문했는지, 현재 주문 상태는 어떤지(결제 완료, 배송 중 등)를 기록해.
- **order_items (주문 상품):** 하나의 주문에 어떤 상품들이 몇 개씩 포함되었는지 상세 내역을 저장해. `orders` 테이블과는 1:N 관계가 되겠지.
- **carts (장바구니):** 고객이 구매하기 위해 담아둔 상품 목록. 이건 세션으로 관리할 수도 있지만, 로그인한 회원의 장바구니 정보를 유지하려면 테이블로 만드는 게 더 좋아.
이 정도가 가장 기본적인 구성이야. 물론 실제 서비스에서는 리뷰, 쿠폰, Q&A 등 더 많은 테이블이 필요하겠지만, 일단 이 뼈대를 잘 만드는 게 중요해.
2. PostgreSQL 스타일로 테이블 만들기 (CREATE TABLE)
이제 위에서 구상한 테이블들을 실제 SQL 쿼리로 만들어 볼 거야. Postgres는 표준 SQL을 잘 따르면서도 유용한 데이터 타입들을 제공해. 이걸 잘 활용하는 게 포인트야.
SERIAL PRIMARY KEY: 자동으로 증가하는 정수형 기본 키를 만들 때 사용해. `BIGSERIAL`은 더 큰 숫자를 지원해.VARCHAR(n): 최대 n글자의 문자열. `TEXT`는 길이 제한 없는 문자열.DECIMAL(p, s): 정확한 소수점 계산이 필요할 때 사용. `p`는 총 자릿수, `s`는 소수점 이하 자릿수. 돈 계산에 필수!TIMESTAMPTZ: 타임존 정보까지 포함하는 타임스탬프. 글로벌 서비스를 생각한다면 `TIMESTAMP`보다 이게 훨씬 좋아.JSONB: JSON 데이터를 바이너리 형태로 저장해. 텍스트로 저장하는 `JSON`보다 인덱싱도 가능하고 쿼리 속도도 빨라. 상품의 추가 속성(색상, 사이즈 등)을 저장하기에 안성맞춤!
아래는 위 테이블들을 생성하는 SQL 예시야. 이걸 참고해서 너만의 스키마를 만들어봐.
-- 회원 테이블
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
username VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
-- 카테고리 테이블
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_id INTEGER REFERENCES categories(id) ON DELETE SET NULL -- 부모-자식 관계
);
-- 상품 테이블
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
attributes JSONB, -- 상품 추가 속성 (예: {"color": "red", "size": "L"})
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
-- 주문 테이블
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_price DECIMAL(12, 2) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- (pending, paid, shipping, completed, cancelled)
shipping_address TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
-- 주문 아이템 테이블
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
quantity INTEGER NOT NULL,
price_per_unit DECIMAL(10, 2) NOT NULL -- 주문 시점의 상품 가격을 저장
);
이 SQL 쿼리들을 psql 커맨드 라인이나 pgAdmin 같은 GUI 툴을 사용해서 데이터베이스에 실행하면, 우리 이커머스 사이트의 뼈대가 완성되는 거야.
특히 `REFERENCES` 키워드를 사용한 외래 키(Foreign Key) 설정은 데이터의 관계를 명확히 하고, `ON DELETE` 옵션으로 관계된 데이터가 삭제될 때 어떻게 동작할지 정의해서 데이터 무결성을 지켜주는 아주 중요한 부분이니 꼭 이해하고 넘어가야 해!
기능 구현: 진짜 코딩의 시작
자, 이제부터가 진짜 재밌는 부분이야. 설계도를 바탕으로 집을 짓는 것처럼, 우리가 설계한 데이터베이스를 기반으로 실제 동작하는 기능을 만들어 볼 거야.
코드이그나이터의 강력한 **MVC(Model-View-Controller) 패턴**을 따라서 코드를 깔끔하게 정리해 보자. MVC가 뭐냐고? 간단히 말해 '역할 분담'이야.
- **Model:** 데이터베이스와 직접 대화하는 역할. 데이터 가져오기, 저장하기, 수정하기, 삭제하기(CRUD)를 담당해.
- **View:** 사용자에게 보여지는 화면(UI). HTML, CSS, Javascript 코드가 여기에 해당돼.
- **Controller:** 모델과 뷰 사이의 중간 관리자. 사용자의 요청을 받아서, 모델에게 데이터를 요청하고, 그 받은 데이터를 뷰에 전달해서 최종 화면을 만들어내.
이 패턴을 따르면 코드가 뒤죽박죽 섞이는 걸 막고, 유지보수하기 훨씬 쉬워져. 그럼 첫 번째 기능부터 만들어볼까?
1. 회원가입 & 로그인 (User Authentication)
어떤 서비스든 기본 중의 기본이지. 사용자가 우리 사이트의 회원이 되고, 로그인해서 자신만의 공간을 가질 수 있게 만들어야 해.
A. 모델 (Model) 만들기
먼저 `app/Models/` 폴더 안에 `UserModel.php` 파일을 만들자. 이 모델은 `users` 테이블과 관련된 모든 작업을 처리할 거야.
<?php
namespace App\Models;
use CodeIgniter\Model;
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $allowedFields = ['email', 'username', 'password_hash'];
// 날짜 자동 기록
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
// 비밀번호 해싱을 위한 콜백
protected $beforeInsert = ['hashPassword'];
protected function hashPassword(array $data)
{
if (! isset($data['data']['password_hash'])) {
return $data;
}
$data['data']['password_hash'] = password_hash($data['data']['password_hash'], PASSWORD_DEFAULT);
return $data;
}
}
CI4의 모델은 정말 강력해. `$table`, `$primaryKey` 같은 몇 가지 속성만 설정해주면 기본적인 CRUD 기능을 알아서 다 제공해줘.
특히 주목할 부분은 `$beforeInsert` 콜백이야. `insert` 되기 직전에 `hashPassword` 함수를 자동으로 실행해서, 우리가 컨트롤러에서 받은 평문 비밀번호를 안전한 해시값으로 바꿔서 DB에 저장해줘. 보안을 위해 아주 중요한 부분이지!
B. 컨트롤러 (Controller) 만들기
다음은 `app/Controllers/` 폴더에 `AuthController.php` 파일을 만들자. 사용자의 회원가입, 로그인, 로그아웃 요청을 처리하는 중간 관리자야.
<?php
namespace App\Controllers;
use App\Models\UserModel;
class AuthController extends BaseController
{
public function register()
{
// 회원가입 폼 보여주기
return view('auth/register');
}
public function attemptRegister()
{
$userModel = new UserModel();
// 폼 데이터 받기
$data = [
'email' => $this->request->getPost('email'),
'username' => $this->request->getPost('username'),
'password_hash' => $this->request->getPost('password'),
];
// 데이터베이스에 저장
if ($userModel->save($data)) {
// 성공 시 로그인 페이지로 리다이렉트
return redirect()->to('/login')->with('message', '회원가입 성공! 로그인해주세요.');
} else {
// 실패 시 에러와 함께 이전 페이지로
return redirect()->back()->withInput()->with('errors', $userModel->errors());
}
}
public function login()
{
// 로그인 폼 보여주기
return view('auth/login');
}
public function attemptLogin()
{
$userModel = new UserModel();
$session = session();
$email = $this->request->getPost('email');
$password = $this->request->getPost('password');
// 이메일로 사용자 찾기
$user = $userModel->where('email', $email)->first();
if ($user && password_verify($password, $user['password_hash'])) {
// 로그인 성공
$sessionData = [
'user_id' => $user['id'],
'username' => $user['username'],
'isLoggedIn' => TRUE
];
$session->set($sessionData);
return redirect()->to('/')->with('message', '로그인 성공!');
} else {
// 로그인 실패
return redirect()->back()->withInput()->with('error', '이메일 또는 비밀번호가 틀렸습니다.');
}
}
public function logout()
{
session()->destroy();
return redirect()->to('/')->with('message', '로그아웃 되었습니다.');
}
}
컨트롤러에서는 `UserModel`을 불러와서 사용자의 입력을 처리하고, 성공/실패에 따라 다른 페이지로 안내하는 역할을 해. `password_verify()` 함수로 해시된 비밀번호를 검증하고, 로그인 성공 시 `session()` 헬퍼를 사용해서 사용자 정보를 세션에 저장하는 걸 볼 수 있어.
C. 뷰 (View) 만들기
마지막으로 사용자에게 보여줄 화면을 만들어야지. `app/Views/auth/` 폴더를 만들고 그 안에 `register.php`와 `login.php` 파일을 만들자. 이건 간단한 HTML 폼이야.
<!-- 에러 메시지 표시 -->
<?php if (session()->get('errors')) : ?>
<div class="alert alert-danger">
<?php foreach (session()->get('errors') as $error) : ?>
<p><?= $error ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form action="/register" method="post">
<?= csrf_field() ?> <!-- CSRF 보호 -->
<label for="email">이메일</label>
<input type="email" name="email" value="<?= old('email') ?>" required>
<label for="username">사용자명</label>
<input type="text" name="username" value="<?= old('username') ?>" required>
<label for="password">비밀번호</label>
<input type="password" name="password" required>
<button type="submit">회원가입</button>
</form>
뷰에서는 `csrf_field()` 함수로 CSRF 공격을 막고, `old()` 함수로 폼 유효성 검사 실패 시 이전에 입력했던 값을 다시 채워주는 등 CI4가 제공하는 편리한 기능들을 활용할 수 있어.
D. 라우트 (Routes) 설정
마지막으로 `app/Config/Routes.php` 파일에서 URL과 컨트롤러의 메소드를 연결해줘야 해.
$routes->get('/register', 'AuthController::register');
$routes->post('/register', 'AuthController::attemptRegister');
$routes->get('/login', 'AuthController::login');
$routes->post('/login', 'AuthController::attemptLogin');
$routes->get('/logout', 'AuthController::logout');
이제 `/register`로 접속하면 회원가입 폼이 보이고, `/login`으로 접속하면 로그인 폼이 보일 거야. 완벽해!
2. 상품 목록과 상세 페이지
회원 기능이 완성됐으니 이제 우리 가게의 물건들을 진열해야지. 상품을 보여주는 페이지를 만들어보자. 이런 상품 리스팅 로직은 **재능넷** 같은 플랫폼에서 전문가들의 재능을 보여주는 방식과 비슷해. 핵심은 정보를 명확하고 매력적으로 보여주는 거지.
먼저 `ProductModel`과 `ProductController`를 위와 비슷한 방식으로 만들어. `ProductModel`은 `products` 테이블과 연결되고, `ProductController`는 상품 목록(`index`)과 상세 정보(`show`)를 보여주는 메소드를 가질 거야.
<?php
namespace App\Controllers;
use App\Models\ProductModel;
class ProductController extends BaseController
{
public function index()
{
$productModel = new ProductModel();
// 모든 상품을 가져와서 뷰에 전달 (페이지네이션 추가)
$data = [
'products' => $productModel->paginate(12), // 한 페이지에 12개씩
'pager' => $productModel->pager,
];
return view('products/index', $data);
}
public function show($id)
{
$productModel = new ProductModel();
// ID로 특정 상품을 찾아서 뷰에 전달
$data['product'] = $productModel->find($id);
if (empty($data['product'])) {
throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find the product: '. $id);
}
return view('products/show', $data);
}
}
CI4 모델의 `paginate()` 기능은 정말 편리해. 단 한 줄로 페이지네이션 로직을 구현하고, 뷰에서는 `$pager->links()`를 호출하기만 하면 페이지 번호 링크가 자동으로 생성돼.
뷰 파일(`app/Views/products/index.php`)에서는 컨트롤러에서 넘겨받은 `$products` 배열을 `foreach` 루프로 돌면서 상품 정보를 예쁘게 표시해주면 돼.
3. 장바구니 기능 (Shopping Cart)
온라인 쇼핑몰의 꽃, 장바구니 기능이야. 사용자가 마음에 드는 상품을 담아둘 수 있게 해보자. 장바구니는 보통 **세션(Session)**을 이용해서 구현해. 세션은 서버에 임시로 데이터를 저장하는 공간이라고 생각하면 돼.
`CartController.php`를 만들고, 장바구니에 상품을 추가(`add`), 보여주기(`show`), 수량 변경(`update`), 삭제(`remove`)하는 메소드들을 구현해보자.
<?php
namespace App\Controllers;
use App\Models\ProductModel;
class CartController extends BaseController
{
public function add()
{
$session = session();
$productModel = new ProductModel();
$productId = $this->request->getPost('product_id');
$quantity = (int)$this->request->getPost('quantity');
$product = $productModel->find($productId);
if (!$product) {
return redirect()->back()->with('error', '상품을 찾을 수 없습니다.');
}
// 세션에서 현재 장바구니 가져오기
$cart = $session->get('cart') ?? [];
// 장바구니에 이미 상품이 있는지 확인
if (isset($cart[$productId])) {
$cart[$productId]['quantity'] += $quantity;
} else {
$cart[$productId] = [
'name' => $product['name'],
'price' => $product['price'],
'quantity' => $quantity,
];
}
// 세션에 장바구니 정보 업데이트
$session->set('cart', $cart);
return redirect()->to('/cart')->with('message', '장바구니에 상품을 담았습니다.');
}
public function show()
{
$data['cart'] = session()->get('cart');
return view('cart/show', $data);
}
// ... update, remove 메소드 구현 ...
}
코드를 보면, 세션에서 `cart`라는 키로 배열을 가져와서 상품 정보를 추가하거나 수정한 뒤, 다시 세션에 저장하는 간단한 로직이야. 이걸 응용해서 수량 변경이나 삭제 기능도 쉽게 만들 수 있겠지?
4. 주문 및 결제 (Checkout Process)
대망의 마지막 단계, 주문 처리야. 사용자가 장바구니에 담은 상품들을 실제로 구매하는 과정이지. 이 부분은 돈과 직접적으로 관련되어 있기 때문에, 데이터의 **정확성**과 **일관성**이 무엇보다 중요해. 여기서 PostgreSQL의 강력한 **트랜잭션(Transaction)** 기능이 빛을 발하게 돼.
트랜잭션이 뭐냐고? 'All or Nothing', 즉 '전부 성공하거나, 전부 실패하거나'를 보장하는 기능이야. 주문 처리 과정은 여러 단계로 이루어져:
- `orders` 테이블에 주문 정보 생성
- 장바구니 상품들을 `order_items` 테이블에 기록
- `products` 테이블의 재고 수량 감소
- 장바구니 비우기
만약 3번 단계(재고 감소)에서 오류가 발생했는데 1, 2번은 이미 DB에 저장되었다면? 데이터가 완전히 꼬여버리겠지. 주문은 들어왔는데 재고는 그대로인 상황이 발생하니까. 트랜잭션은 이 모든 과정을 하나의 묶음으로 처리해서, 중간에 하나라도 실패하면 모든 작업을 없었던 일로 되돌려(Rollback)줘. 정말 믿음직하지?
`OrderController.php`에 주문을 처리하는 `placeOrder` 메소드를 만들어보자.
<?php
namespace App\Controllers;
use App\Models\OrderModel;
use App\Models\OrderItemModel;
use App\Models\ProductModel;
class OrderController extends BaseController
{
public function placeOrder()
{
$session = session();
$cart = $session->get('cart');
$userId = $session->get('user_id');
if (empty($cart) || !$userId) {
return redirect()->to('/cart')->with('error', '주문할 상품이 없거나 로그인이 필요합니다.');
}
$db = \Config\Database::connect();
$db->transStart(); // 트랜잭션 시작!
try {
// 1. orders 테이블에 주문 생성
$orderModel = new OrderModel();
$totalPrice = array_reduce($cart, fn($sum, $item) => $sum + ($item['price'] * $item['quantity']), 0);
$orderId = $orderModel->insert([
'user_id' => $userId,
'total_price' => $totalPrice,
'status' => 'paid' // 실제로는 결제 모듈 연동 후 상태 변경
]);
// 2. order_items 테이블에 상품 기록
$orderItemModel = new OrderItemModel();
$productModel = new ProductModel();
foreach ($cart as $productId => $item) {
$orderItemModel->insert([
'order_id' => $orderId,
'product_id' => $productId,
'quantity' => $item['quantity'],
'price_per_unit' => $item['price']
]);
// 3. products 테이블 재고 감소
$productModel->where('id', $productId)->decrement('stock', $item['quantity']);
}
// 4. 장바구니 비우기
$session->remove('cart');
$db->transComplete(); // 모든 작업이 성공했으면 커밋!
return redirect()->to('/orders/success')->with('message', '주문이 성공적으로 완료되었습니다.');
} catch (\Exception $e) {
$db->transRollback(); // 오류 발생 시 모든 작업 롤백!
log_message('error', '[ERROR] Order placement failed: ' . $e->getMessage());
return redirect()->back()->with('error', '주문 처리 중 오류가 발생했습니다. 다시 시도해주세요.');
}
}
}
`$db->transStart()`로 트랜잭션을 시작하고, 모든 DB 작업이 끝난 후 `$db->transComplete()`를 호출해. 만약 `try-catch` 블록 안에서 어떤 예외(Exception)라도 발생하면 `catch` 블록으로 이동해서 `$db->transRollback()`이 실행되어 모든 변경 사항이 취소돼. 이렇게 하면 우리 쇼핑몰의 데이터는 언제나 안전하게 유지될 수 있어.
레벨업! 고수처럼 보이기 위한 꿀팁
좋아, 지금까지 핵심 기능들을 모두 구현했어. 이 정도만 해도 훌륭한 이커머스 사이트의 기본은 갖춘 셈이야. 하지만 진짜 프로 개발자는 여기서 멈추지 않지.
코드를 더 효율적으로 관리하고, 테스트를 쉽게 만들고, 보안과 성능까지 챙기는 몇 가지 고급 기술들을 알아보자. 이걸 알면 너도 이제 '고수' 소리를 들을 수 있을 거야!
1. 데이터베이스 마이그레이션 (Migrations)
우리가 아까 `CREATE TABLE` SQL을 직접 실행해서 테이블을 만들었잖아? 혼자 개발할 땐 괜찮지만, 여러 명의 팀원과 함께 일하거나, 나중에 테이블 구조를 변경해야 할 때 문제가 생길 수 있어. "어? 나 아직 저번에 바꾼 컬럼 추가 안 했는데?" 같은 상황이 발생하는 거지.
**마이그레이션**은 이런 문제를 해결해줘. 데이터베이스의 스키마 변경 이력을 코드로 관리하는 거야. 마치 Git으로 코드 버전을 관리하는 것처럼 말이지.
터미널에서 아래 명령어로 마이그레이션 파일을 생성할 수 있어.
php spark make:migration create_users_table
그럼 `app/Database/Migrations/` 폴더에 타임스탬프가 찍힌 파일이 하나 생길 거야. 그 파일을 열어보면 `up()`과 `down()` 메소드가 있어.
- `up()`: 테이블을 생성하거나 컬럼을 추가하는 등, 스키마를 '업그레이드'하는 코드를 작성해.
- `down()`: `up()`에서 했던 작업을 되돌리는(롤백) 코드를 작성해. 테이블을 삭제하거나 컬럼을 지우는 거지.
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateUsersTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'BIGSERIAL', 'auto_increment' => true],
'email' => ['type' => 'VARCHAR', 'constraint' => '255', 'unique' => true],
'password_hash' => ['type' => 'VARCHAR', 'constraint' => '255'],
'created_at' => ['type' => 'TIMESTAMPTZ', 'null' => true],
'updated_at' => ['type' => 'TIMESTAMPTZ', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('users');
}
public function down()
{
$this->forge->dropTable('users');
}
}
이렇게 코드로 스키마를 정의해두고, 터미널에서 `php spark migrate` 명령만 실행하면 CI4가 알아서 최신 버전까지 모든 스키마 변경을 적용해줘. 팀원 누구든 이 명령 하나로 동일한 데이터베이스 구조를 가질 수 있게 되는 거지. 정말 체계적이지?
2. 데이터 시더 (Seeders)
개발 초기에는 테스트를 위해 데이터가 필요하잖아. 상품도 몇 개 등록해보고, 회원도 몇 명 가입시켜봐야 화면이 제대로 나오는지 확인할 수 있으니까. 매번 수동으로 가입하고 상품을 등록하는 건 너무 귀찮은 일이야.
**시더(Seeder)**는 데이터베이스에 초기 데이터를 '심어주는' 역할을 해. 테스트용 더미 데이터를 대량으로, 그리고 자동으로 생성할 수 있어.
php spark make:seeder UserSeeder
위 명령어로 `app/Database/Seeds/UserSeeder.php` 파일을 만들고, `run()` 메소드 안에 데이터를 생성하는 코드를 작성하면 돼.
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class UserSeeder extends Seeder
{
public function run()
{
$data = [
[
'email' => 'testuser1@example.com',
'username' => '테스트유저1',
'password_hash' => password_hash('1234', PASSWORD_DEFAULT)
],
[
'email' => 'testuser2@example.com',
'username' => '테스트유저2',
'password_hash' => password_hash('1234', PASSWORD_DEFAULT)
]
];
// Simple Queries
// $this->db->query('INSERT INTO users (username, email) VALUES(:username:, :email:)', $data);
// Using Query Builder
$this->db->table('users')->insertBatch($data);
}
}
이제 터미널에서 아래 명령어를 실행하면?
php spark db:seed UserSeeder
짠! `users` 테이블에 테스트용 사용자 2명이 순식간에 만들어졌을 거야. 상품 시더, 카테고리 시더 등도 만들어서 `php spark db:seed` 한방에 모든 테스트 데이터를 준비할 수 있어. 개발 효율이 엄청나게 올라가겠지?
3. 보안, 아무리 강조해도 지나치지 않아
우리가 만든 사이트는 고객의 개인정보와 결제 정보가 오고 가는 소중한 공간이야. 보안은 선택이 아니라 필수지. 다행히 코드이그나이터는 기본적인 보안 기능들을 잘 갖추고 있어.
- **CSRF (Cross-Site Request Forgery) 보호:** 내가 보낸 요청인 척 위조된 요청을 보내는 공격이야. CI4는 기본적으로 이 기능이 활성화되어 있고, 폼에 `= csrf_field() ?>`만 추가해주면 알아서 방어해줘.
- **XSS (Cross-Site Scripting) 필터링:** 게시판이나 상품평처럼 사용자가 입력한 내용에 악성 스크립트를 심는 공격이야. CI4의 `esc()` 함수를 사용해서 출력할 데이터를 감싸주면 안전하게 처리할 수 있어. `= esc($product['name']) ?>` 처럼 말이지.
- **SQL 인젝션 방어:** CI4의 쿼리 빌더나 모델을 사용하면 모든 입력값이 자동으로 이스케이프(escape) 처리돼서 SQL 인젝션 공격을 원천적으로 차단해줘. 절대로 사용자의 입력을 그대로 SQL 쿼리 문자열에 합쳐서 사용하면 안 돼!
이런 기본적인 수칙만 잘 지켜도 대부분의 웹 공격으로부터 우리 사이트를 안전하게 지킬 수 있어.
마치며: 너도 이제 이커머스 개발자!
와, 정말 긴 여정이었어. 여기까지 따라오느라 정말 고생 많았어! 👏
우리는 오늘 코드이그나이터와 PostgreSQL이라는 매력적인 조합으로 이커머스 사이트를 만들기 위한 거의 모든 과정을 훑어봤어.
개발 환경을 세팅하는 것부터 시작해서, 데이터베이스의 뼈대를 설계하고, MVC 패턴에 따라 회원가입, 상품 전시, 장바구니, 그리고 가장 중요한 주문 처리 기능까지 직접 구현해봤지. 마지막으로 마이그레이션과 시더를 통해 개발 프로세스를 더 세련되게 만들고, 기본적인 보안 수칙까지 점검했어.
물론 이게 끝은 아니야. 실제 상용 서비스를 위해서는 PG(결제 대행사) 모듈 연동, 관리자 페이지 제작, 상품 검색 기능, 배송 추적, 소셜 로그인, 리뷰 기능 등등... 만들어야 할 기능들이 산더미처럼 남아있지. 하지만 겁먹을 필요 없어. 가장 중요한 핵심 뼈대를 만드는 방법을 배웠으니까, 이제 어떤 기능이든 이 구조 위에 차곡차곡 쌓아 올릴 수 있을 거야.
오늘 배운 내용이 너의 개발자 커리어에 멋진 무기가 되기를 바라. 중요한 건 이론을 아는 것에서 그치지 않고, 직접 너만의 프로젝트를 시작해보는 거야.
**"Just do it!"**
작은 장난감 가게든, 네가 좋아하는 책을 파는 서점이든, 뭐든 좋아. 오늘 배운 지식을 바탕으로 너만의 아이디어를 현실로 만들어봐. 그 과정에서 겪는 수많은 에러와 삽질이 너를 진짜 '찐개발자'로 만들어 줄 테니까.
그럼, 너의 멋진 프로젝트를 응원하며! 다음에 또 재밌는 주제로 만나자고. 안녕!
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.