Ver3.0 ๐ ์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋: ์ฅ๋ฐ๊ตฌ๋๋ถํฐ ๊ฒฐ์ , ์ฃผ๋ฌธ ์์คํ ๊น์ง ์ค์ ๊ตฌํ ๋ ธํ์ฐ

๐ ์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋: ์ฅ๋ฐ๊ตฌ๋๋ถํฐ ๊ฒฐ์ , ์ฃผ๋ฌธ ์์คํ ๊น์ง ์ค์ ๊ตฌํ ๋ ธํ์ฐ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ์ผํ๋ชฐ ์ฑ ๊ฐ๋ฐ์ ๋ชจ๋ ๊ฒ ๐ก
์๋
! ๐ ์ค๋์ ์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ์์ ๊ฐ์ฅ ํต์ฌ์ ์ธ ๋ถ๋ถ์ธ ์ฅ๋ฐ๊ตฌ๋, ๊ฒฐ์ , ์ฃผ๋ฌธ ์์คํ
์ ํจ๊ป ํํค์ณ๋ณผ ๊ฑฐ์ผ. ์์งํ ๋งํ๋ฉด ์ด ์ธ ๊ฐ์ง๋ง ์ ๋๋ก ๊ตฌํํด๋ ์ผํ๋ชฐ ์ฑ์ 80%๋ ์์ฑํ ๊ฑฐ๋ ๋ง์ฐฌ๊ฐ์ง๊ฑฐ๋ ! ๐
์์ฆ ๋ชจ๋ฐ์ผ ์ผํ ์์ฅ์ด ์ผ๋ง๋ ๋จ๊ฑฐ์ด์ง ์์ง? ํต๊ณ์ ๋ฐ๋ฅด๋ฉด 2023๋
๊ธฐ์ค ๊ตญ๋ด ๋ชจ๋ฐ์ผ ์ผํ ๊ฑฐ๋์ก์ด 100์กฐ ์์ ๋์ด์ฐ๋. ๋ฏธ์น ์ซ์ ์๋์ผ? ๐ ๊ทธ๋งํผ ์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ์์ ๋ํ ์์๋ ์์ฒญ๋๊ฒ ๋์ด๋๊ณ ์์ด.
๋๋ ์ฒ์ ์ด์ปค๋จธ์ค ์ฑ์ ๊ฐ๋ฐํ ๋๋ "๊ทธ๋ฅ ๋ฌผ๊ฑด ๋ด๊ณ ๊ฒฐ์ ํ๋ฉด ๋๋ ๊ฑฐ ์๋?"๋ผ๊ณ ์๊ฐํ์์ด. ๊ทผ๋ฐ ๋ง์ ํด๋ณด๋๊น... ์, ์ง์ง ๊ณ ๋ คํด์ผ ํ ๊ฒ ์ฐ๋๋ฏธ๋๋ผ๊ณ . ๐
์ฌ๊ณ ๊ด๋ฆฌ, ๋์์ฑ ์ ์ด, ๊ฒฐ์ ์คํจ ์ฒ๋ฆฌ, ํ๋ถ ๋ก์ง... ์๊ฐ๋ง ํด๋ ๋จธ๋ฆฌ๊ฐ ์ง๋์ง๋ํ์ง?
๊ทธ๋์ ์ค๋์ ๋ด๊ฐ ์ค์ ๋ก ์ฌ๋ฌ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋ฐฐ์ด ๋
ธํ์ฐ๋ค์ ์๋์์ด ๊ณต์ ํด๋ณผ๊ฒ. ์ด๋ก ๋ง ์ฃผ๊ตฌ์ฅ์ฐฝ ๋์ด๋๋ ๊ฒ ์๋๋ผ, ์ค์ ์์ ๋ฐ๋ก ์จ๋จน์ ์ ์๋ ํ๋ค ์์ฃผ๋ก ๋ง์ด์ผ! ๐ช
๐ฏ ์ด์ปค๋จธ์ค ์ฑ์ ํต์ฌ ๊ตฌ์กฐ ์ดํดํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ๋ค์ด๊ฐ๊ธฐ ์ ์, ์ด์ปค๋จธ์ค ์ฑ์ ์ ์ฒด์ ์ธ ๊ตฌ์กฐ๋ถํฐ ํ๋ฒ ์ง๊ณ ๋์ด๊ฐ์. ํฐ ๊ทธ๋ฆผ์ ์ดํดํ๊ณ ์์ด์ผ ์ธ๋ถ ๊ตฌํํ ๋๋ ํท๊ฐ๋ฆฌ์ง ์๊ฑฐ๋ !
์ด์ปค๋จธ์ค ์ฑ์ ํฌ๊ฒ ํ๋ก ํธ์๋(์ฌ์ฉ์๊ฐ ๋ณด๋ ํ๋ฉด)์ ๋ฐฑ์๋(์๋ฒ ๋ก์ง)๋ก ๋๋์ด. ๋น์ฐํ ์๊ธฐ์ง๋ง, ์ด ๋์ด ์ ๊ธฐ์ ์ผ๋ก ์ ์ฐ๊ฒฐ๋์ด์ผ ์ ๋๋ก ๋ ์ผํ ๊ฒฝํ์ ์ ๊ณตํ ์ ์์ด.
โข ์ํ ๋ชฉ๋ก ๋ฐ ์์ธ ํ๋ฉด
โข ์ฅ๋ฐ๊ตฌ๋ UI/UX
โข ๊ฒฐ์ ํ๋ก์ธ์ค ํ๋ฉด
โข ์ฃผ๋ฌธ ๋ด์ญ ๋ฐ ๋ฐฐ์ก ์ถ์
โข ์ฌ์ฉ์ ์ธ์ฆ ๋ฐ ํ๋กํ ๊ด๋ฆฌ
โข ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ
โข ์ฅ๋ฐ๊ตฌ๋ ์ธ์ ๊ด๋ฆฌ
โข ๊ฒฐ์ ๊ฒ์ดํธ์จ์ด ์ฐ๋
โข ์ฃผ๋ฌธ ์ฒ๋ฆฌ ๋ฐ ์ฌ๊ณ ๊ด๋ฆฌ
โข ์ฌ์ฉ์ ์ธ์ฆ ๋ฐ ๊ถํ ๊ด๋ฆฌ
์์ฆ์ ๋ค์ดํฐ๋ธ ์ฑ(Swift, Kotlin)๋ณด๋ค๋ React Native๋ Flutter ๊ฐ์ ํฌ๋ก์ค ํ๋ซํผ ํ๋ ์์ํฌ๋ฅผ ๋ง์ด ์ฌ์ฉํ๋ ์ถ์ธ์ผ. ํ ๋ฒ ๊ฐ๋ฐํด์ iOS์ Android ๋ ๋ค ์ปค๋ฒํ ์ ์์ผ๋๊น ๊ฐ๋ฐ ๋น์ฉ์ด ํ ์ค์ด๋ค๊ฑฐ๋ ! ๐ฐ
๋ฐฑ์๋๋ Node.js, Python(Django/Flask), Java(Spring Boot) ๋ฑ ๋ค์ํ ์ ํ์ง๊ฐ ์์ด. ๊ฐ์ธ์ ์ผ๋ก๋ Node.js + Express ์กฐํฉ์ ์ถ์ฒํ๋๋ฐ, JavaScript๋ก ํ๋ก ํธ์ ๋ฐฑ์ ๋ชจ๋ ๋ค๋ฃฐ ์ ์์ด์ ๋ฌ๋ ์ปค๋ธ๊ฐ ๋ฎ๊ฑฐ๋ .
๐ ์ฅ๋ฐ๊ตฌ๋ ์์คํ ๊ตฌํํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฅ๋ฐ๊ตฌ๋๋ถํฐ ํํค์ณ๋ณด์! ์ฅ๋ฐ๊ตฌ๋๋ ๊ฒ๋ณด๊ธฐ์ ๋จ์ํด ๋ณด์ด์ง๋ง, ์ค์ ๋ก๋ ๊ฝค ๋ณต์กํ ๋ก์ง์ด ์จ์ด์์ด. ๐ค
๐พ ์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ ์ ์ฅ ๋ฐฉ์
์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ๋ฅผ ์ด๋์ ์ ์ฅํ ์ง๋ถํฐ ๊ฒฐ์ ํด์ผ ํด. ํฌ๊ฒ ์ธ ๊ฐ์ง ๋ฐฉ์์ด ์์ด:
์ฌ์ฉ์์ ๊ธฐ๊ธฐ์ ์ง์ ์ ์ฅํ๋ ๋ฐฉ์์ด์ผ. ๊ฐ์ฅ ๊ฐ๋จํ๊ณ ์๋ฒ ๋ถ๋ด์ด ์์ง๋ง, ๊ธฐ๊ธฐ๋ฅผ ๋ฐ๊พธ๋ฉด ์ฅ๋ฐ๊ตฌ๋๊ฐ ์ฌ๋ผ์ง๋ค๋ ๋จ์ ์ด ์์ด. ๐ข
// React Native AsyncStorage ์์
import AsyncStorage from '@react-native-async-storage/async-storage';
const addToCart = async (product) => {
try {
const existingCart = await AsyncStorage.getItem('cart');
const cart = existingCart ? JSON.parse(existingCart) : [];
const existingItem = cart.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
cart.push({ ...product, quantity: 1 });
}
await AsyncStorage.setItem('cart', JSON.stringify(cart));
} catch (error) {
console.error('์ฅ๋ฐ๊ตฌ๋ ์ ์ฅ ์คํจ:', error);
}
};
์๋ฒ์ ์ธ์ ์ ๋ง๋ค์ด์ ์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ์. ๋ก๊ทธ์ธํ์ง ์์ ์ฌ์ฉ์๋ ์ธ์ ID๋ก ์ฅ๋ฐ๊ตฌ๋๋ฅผ ์ ์งํ ์ ์์ด. ๐
// Express.js + Redis ์ธ์
์์
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 86400000 } // 24์๊ฐ
}));
app.post('/api/cart/add', (req, res) => {
const { productId, quantity } = req.body;
if (!req.session.cart) {
req.session.cart = [];
}
const existingItem = req.session.cart.find(item => item.productId === productId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
req.session.cart.push({ productId, quantity });
}
res.json({ success: true, cart: req.session.cart });
});
๊ฐ์ฅ ์์ ์ ์ธ ๋ฐฉ์! ์ฌ์ฉ์ ๊ณ์ ์ ์ฅ๋ฐ๊ตฌ๋๋ฅผ ์ฐ๊ฒฐํด์ DB์ ์ ์ฅํด. ์ด๋ค ๊ธฐ๊ธฐ์์๋ ๋ก๊ทธ์ธํ๋ฉด ์ฅ๋ฐ๊ตฌ๋๋ฅผ ๋ณผ ์ ์์ง. ๐ฏ
// MongoDB ์คํค๋ง ์์
const cartSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
items: [{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
quantity: {
type: Number,
required: true,
min: 1
},
price: {
type: Number,
required: true
},
addedAt: {
type: Date,
default: Date.now
}
}],
updatedAt: {
type: Date,
default: Date.now
}
});
// ์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ API
router.post('/cart/add', authenticateUser, async (req, res) => {
try {
const { productId, quantity } = req.body;
const userId = req.user.id;
let cart = await Cart.findOne({ userId });
if (!cart) {
cart = new Cart({ userId, items: [] });
}
const product = await Product.findById(productId);
if (!product) {
return res.status(404).json({ error: '์ํ์ ์ฐพ์ ์ ์์ต๋๋ค' });
}
const existingItem = cart.items.find(
item => item.productId.toString() === productId
);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.items.push({
productId,
quantity,
price: product.price
});
}
cart.updatedAt = Date.now();
await cart.save();
res.json({ success: true, cart });
} catch (error) {
res.status(500).json({ error: '์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ ์คํจ' });
}
});
๐ ์ฅ๋ฐ๊ตฌ๋ ์๋ ๋ณ๊ฒฝ ๋ฐ ์ญ์
์ฅ๋ฐ๊ตฌ๋์ ๋ด๊ธฐ๋ง ํ๋ฉด ๋ญํด? ์๋๋ ์กฐ์ ํ๊ณ ์ญ์ ๋ ํ ์ ์์ด์ผ์ง! ์ด ๋ถ๋ถ์ UI/UX๊ฐ ์ ๋ง ์ค์ํด. ์ฌ์ฉ์๊ฐ ์ง๊ด์ ์ผ๋ก ์กฐ์ํ ์ ์์ด์ผ ํ๊ฑฐ๋ .
// React Native ์ฅ๋ฐ๊ตฌ๋ ์ปดํฌ๋ํธ ์์
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, FlatList } from 'react-native';
const CartScreen = () => {
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
loadCart();
}, []);
useEffect(() => {
calculateTotal();
}, [cartItems]);
const loadCart = async () => {
try {
const response = await fetch('https://api.example.com/cart', {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
const data = await response.json();
setCartItems(data.items);
} catch (error) {
console.error('์ฅ๋ฐ๊ตฌ๋ ๋ก๋ ์คํจ:', error);
}
};
const updateQuantity = async (itemId, newQuantity) => {
if (newQuantity < 1) {
removeItem(itemId);
return;
}
try {
await fetch(`https://api.example.com/cart/update`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify({ itemId, quantity: newQuantity })
});
setCartItems(prevItems =>
prevItems.map(item =>
item.id === itemId ? { ...item, quantity: newQuantity } : item
)
);
} catch (error) {
console.error('์๋ ๋ณ๊ฒฝ ์คํจ:', error);
}
};
const removeItem = async (itemId) => {
try {
await fetch(`https://api.example.com/cart/remove/${itemId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${userToken}`
}
});
setCartItems(prevItems => prevItems.filter(item => item.id !== itemId));
} catch (error) {
console.error('์ํ ์ญ์ ์คํจ:', error);
}
};
const calculateTotal = () => {
const total = cartItems.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
setTotalPrice(total);
};
return (
<View style={{ flex: 1, padding: 16 }}>
<FlatList
data={cartItems}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<View style={{
flexDirection: 'row',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0'
}}>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 16, fontWeight: 'bold' }}>
{item.name}
</Text>
<Text style={{ color: '#666', marginTop: 4 }}>
{item.price.toLocaleString()}์
</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
onPress={() => updateQuantity(item.id, item.quantity - 1)}
style={{
width: 30,
height: 30,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4
}}
>
<Text style={{ fontSize: 18 }}>-</Text>
</TouchableOpacity>
<Text style={{ marginHorizontal: 12, fontSize: 16 }}>
{item.quantity}
</Text>
<TouchableOpacity
onPress={() => updateQuantity(item.id, item.quantity + 1)}
style={{
width: 30,
height: 30,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4
}}
>
<Text style={{ fontSize: 18 }}>+</Text>
</TouchableOpacity>
</View>
</View>
)}
/>
<View style={{
padding: 16,
borderTopWidth: 2,
borderTopColor: '#333'
}}>
<Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 12 }}>
์ด ๊ฒฐ์ ๊ธ์ก: {totalPrice.toLocaleString()}์
</Text>
<TouchableOpacity
style={{
backgroundColor: '#6b9bd1',
padding: 16,
borderRadius: 8,
alignItems: 'center'
}}
onPress={() => navigation.navigate('Checkout')}
>
<Text style={{ color: '#fff', fontSize: 16, fontWeight: 'bold' }}>
๊ฒฐ์ ํ๊ธฐ
</Text>
</TouchableOpacity>
</View>
</View>
);
};
โฐ ์ฅ๋ฐ๊ตฌ๋ ๋ง๋ฃ ์ฒ๋ฆฌ
์ฅ๋ฐ๊ตฌ๋์ ๋ด๊ธด ์ํ์ ์์ํ ๋ณด๊ดํ ์ ์์ด. ํนํ ์ธ๊ธฐ ์ํ์ ๊ฒฝ์ฐ ๋ค๋ฅธ ์ฌ๋๋ค์ด ๊ตฌ๋งคํ์ง ๋ชปํ๊ฒ ๋ง๋ ๊ผด์ด ๋๊ฑฐ๋ . ๊ทธ๋์ ์ฅ๋ฐ๊ตฌ๋ ๋ง๋ฃ ์ ์ฑ
์ด ํ์ํด!
๋ณดํต 30์ผ ์ ๋๋ก ์ค์ ํ๋๋ฐ, ์๋น์ค ํน์ฑ์ ๋ฐ๋ผ ์กฐ์ ํ๋ฉด ๋ผ. ์๋ฅผ ๋ค์ด ํฐ์ผํ
์๋น์ค๋ผ๋ฉด 10๋ถ, ์ผ๋ฐ ์ผํ๋ชฐ์ด๋ผ๋ฉด 30์ผ ์ด๋ฐ ์์ผ๋ก ๋ง์ด์ผ.
// ์ฅ๋ฐ๊ตฌ๋ ๋ง๋ฃ ์ฒ๋ฆฌ ์ค์ผ์ค๋ฌ (Node.js + node-cron)
const cron = require('node-cron');
const Cart = require('./models/Cart');
// ๋งค์ผ ์๋ฒฝ 3์์ ๋ง๋ฃ๋ ์ฅ๋ฐ๊ตฌ๋ ์ ๋ฆฌ
cron.schedule('0 3 * * *', async () => {
try {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const result = await Cart.updateMany(
{ updatedAt: { $lt: thirtyDaysAgo } },
{ $set: { items: [] } }
);
console.log(`${result.modifiedCount}๊ฐ์ ๋ง๋ฃ๋ ์ฅ๋ฐ๊ตฌ๋ ์ ๋ฆฌ ์๋ฃ`);
} catch (error) {
console.error('์ฅ๋ฐ๊ตฌ๋ ์ ๋ฆฌ ์คํจ:', error);
}
});
// ๋๋ ๊ฐ๋ณ ์์ดํ
๋ง๋ฃ ์ฒ๋ฆฌ
const cleanExpiredItems = async (userId) => {
const cart = await Cart.findOne({ userId });
if (!cart) return;
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
cart.items = cart.items.filter(item => item.addedAt > sevenDaysAgo);
await cart.save();
};
๐ณ ๊ฒฐ์ ์์คํ ๊ตฌํํ๊ธฐ
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๊ฒฐ์ ํํธ์ผ! ๐ฐ ์์งํ ๋งํ๋ฉด ๊ฒฐ์ ์์คํ
์ ์ง์ ๊ตฌํํ๋ ๊ฒ ์๋๋ผ PG์ฌ(Payment Gateway)์ API๋ฅผ ์ฐ๋ํ๋ ๊ฑฐ์ผ. ์๋๋ฉด ๊ฒฐ์ ๋ ๋ณด์์ด ์ ๋ง์ ๋ง ์ค์ํ๊ฑฐ๋ !
๊ตญ๋ด์์ ๋ง์ด ์ฐ๋ PG์ฌ๋ก๋ ์์ํฌํธ(PortOne), ํ ์คํ์ด๋จผ์ธ , ๋์ด์คํ์ด, KG์ด๋์์ค ๋ฑ์ด ์์ด. ๋๋ ๊ฐ์ธ์ ์ผ๋ก ์์ํฌํธ๋ฅผ ์ถ์ฒํ๋๋ฐ, ๋ฌธ์ํ๊ฐ ์ ๋์ด ์๊ณ ๊ฐ๋ฐ์ ์นํ์ ์ด๊ฑฐ๋ . ๐
๐ ๊ฒฐ์ ํ๋ก์ธ์ค ์ดํดํ๊ธฐ
๊ฒฐ์ ํ๋ก์ธ์ค๋ฅผ ๋จ๊ณ๋ณ๋ก ๋๋ ๋ณด๋ฉด ์ด๋ ๊ฒ ๋ผ:
| ๋จ๊ณ | ์ค๋ช | ์ฃผ์ ์์ |
|---|---|---|
| 1. ๊ฒฐ์ ์ค๋น | ์ฃผ๋ฌธ ์ ๋ณด ์์ฑ ๋ฐ ๊ฒ์ฆ | ์ฌ๊ณ ํ์ธ, ๊ฐ๊ฒฉ ๊ณ์ฐ, ์ฃผ๋ฌธ๋ฒํธ ์์ฑ |
| 2. ๊ฒฐ์ ์์ฒญ | PG์ฌ ๊ฒฐ์ ์ฐฝ ํธ์ถ | ๊ฒฐ์ ์ ๋ณด ์ ๋ฌ, ์ฌ์ฉ์ ๊ฒฐ์ ์งํ |
| 3. ๊ฒฐ์ ๊ฒ์ฆ | ์๋ฒ์์ ๊ฒฐ์ ๊ฒฐ๊ณผ ํ์ธ | ์๋ณ์กฐ ๊ฒ์ฆ, ๊ธ์ก ์ผ์น ํ์ธ |
| 4. ์ฃผ๋ฌธ ์๋ฃ | ์ฃผ๋ฌธ ์ํ ์ ๋ฐ์ดํธ | ์ฌ๊ณ ์ฐจ๊ฐ, ์ฃผ๋ฌธ ํ์ , ์๋ฆผ ๋ฐ์ก |
๐ฑ ์์ํฌํธ ์ฐ๋ ์์
์ค์ ๋ก ์์ํฌํธ๋ฅผ ์ฐ๋ํ๋ ์ฝ๋๋ฅผ ๋ณด์ฌ์ค๊ฒ. React Native ๊ธฐ์ค์ด์ผ!
// 1. ์์ํฌํธ React Native SDK ์ค์น
// npm install iamport-react-native
// 2. ๊ฒฐ์ ์ปดํฌ๋ํธ ๊ตฌํ
import React from 'react';
import IMP from 'iamport-react-native';
const PaymentScreen = ({ route, navigation }) => {
const { orderData } = route.params;
const handlePaymentResponse = async (response) => {
try {
// ๊ฒฐ์ ๊ฒฐ๊ณผ๋ฅผ ์๋ฒ๋ก ์ ์กํ์ฌ ๊ฒ์ฆ
const verifyResponse = await fetch('https://api.example.com/payment/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify({
imp_uid: response.imp_uid,
merchant_uid: response.merchant_uid
})
});
const result = await verifyResponse.json();
if (result.success) {
// ๊ฒฐ์ ์ฑ๊ณต
navigation.navigate('OrderComplete', { orderId: result.orderId });
} else {
// ๊ฒฐ์ ์คํจ
alert('๊ฒฐ์ ์ ์คํจํ์ต๋๋ค: ' + result.message);
navigation.goBack();
}
} catch (error) {
console.error('๊ฒฐ์ ๊ฒ์ฆ ์คํจ:', error);
alert('๊ฒฐ์ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
navigation.goBack();
}
};
const paymentData = {
pg: 'html5_inicis',
pay_method: 'card',
merchant_uid: orderData.merchantUid,
name: orderData.orderName,
amount: orderData.totalAmount,
buyer_email: orderData.buyerEmail,
buyer_name: orderData.buyerName,
buyer_tel: orderData.buyerTel,
buyer_addr: orderData.buyerAddr,
buyer_postcode: orderData.buyerPostcode,
app_scheme: 'yourapp',
// ์ถ๊ฐ ํ๋ผ๋ฏธํฐ
custom_data: {
orderId: orderData.orderId,
userId: orderData.userId
}
};
return (
<IMP.Payment
userCode="imp12345678" // ์์ํฌํธ ๊ฐ๋งน์ ์๋ณ์ฝ๋
data={paymentData}
callback={handlePaymentResponse}
/>
);
};
// 3. ์๋ฒ ์ธก ๊ฒฐ์ ๊ฒ์ฆ ๋ก์ง (Node.js + Express)
const axios = require('axios');
router.post('/payment/verify', authenticateUser, async (req, res) => {
try {
const { imp_uid, merchant_uid } = req.body;
// 1. ์์ํฌํธ ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
const getToken = await axios.post('https://api.iamport.kr/users/getToken', {
imp_key: process.env.IMP_API_KEY,
imp_secret: process.env.IMP_API_SECRET
});
const accessToken = getToken.data.response.access_token;
// 2. ๊ฒฐ์ ์ ๋ณด ์กฐํ
const getPaymentData = await axios.get(
`https://api.iamport.kr/payments/${imp_uid}`,
{ headers: { 'Authorization': accessToken } }
);
const paymentData = getPaymentData.data.response;
// 3. ์ฃผ๋ฌธ ์ ๋ณด ์กฐํ
const order = await Order.findOne({ merchantUid: merchant_uid });
if (!order) {
return res.status(404).json({
success: false,
message: '์ฃผ๋ฌธ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
// 4. ๊ฒฐ์ ๊ธ์ก ๊ฒ์ฆ
if (paymentData.amount !== order.totalAmount) {
// ๊ธ์ก ๋ถ์ผ์น - ์๋ณ์กฐ ์์ฌ
await Order.updateOne(
{ _id: order._id },
{ status: 'payment_failed', failReason: '๊ฒฐ์ ๊ธ์ก ๋ถ์ผ์น' }
);
return res.status(400).json({
success: false,
message: '๊ฒฐ์ ๊ธ์ก์ด ์ผ์นํ์ง ์์ต๋๋ค'
});
}
// 5. ๊ฒฐ์ ์ํ ํ์ธ
if (paymentData.status === 'paid') {
// ๊ฒฐ์ ์ฑ๊ณต ์ฒ๋ฆฌ
await Order.updateOne(
{ _id: order._id },
{
status: 'paid',
paidAt: new Date(),
impUid: imp_uid,
paymentMethod: paymentData.pay_method
}
);
// ์ฌ๊ณ ์ฐจ๊ฐ
for (const item of order.items) {
await Product.updateOne(
{ _id: item.productId },
{ $inc: { stock: -item.quantity } }
);
}
// ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
await Cart.updateOne(
{ userId: req.user.id },
{ $set: { items: [] } }
);
// ์ฃผ๋ฌธ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก (์๋ต)
res.json({
success: true,
orderId: order._id,
message: '๊ฒฐ์ ๊ฐ ์๋ฃ๋์์ต๋๋ค'
});
} else {
// ๊ฒฐ์ ์คํจ
await Order.updateOne(
{ _id: order._id },
{ status: 'payment_failed', failReason: paymentData.fail_reason }
);
res.status(400).json({
success: false,
message: paymentData.fail_reason || '๊ฒฐ์ ์ ์คํจํ์ต๋๋ค'
});
}
} catch (error) {
console.error('๊ฒฐ์ ๊ฒ์ฆ ์ค๋ฅ:', error);
res.status(500).json({
success: false,
message: '๊ฒฐ์ ๊ฒ์ฆ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
});
๐ ๊ฒฐ์ ์คํจ ๋ฐ ์ทจ์ ์ฒ๋ฆฌ
๊ฒฐ์ ๊ฐ ํญ์ ์ฑ๊ณตํ๋ ๊ฑด ์๋์ผ. ์นด๋ ํ๋ ์ด๊ณผ, ๋คํธ์ํฌ ์ค๋ฅ, ์ฌ์ฉ์๊ฐ ์ค๊ฐ์ ์ทจ์ํ๋ ๊ฒฝ์ฐ ๋ฑ ๋ค์ํ ์ด์ ๋ก ์คํจํ ์ ์์ด. ์ด๋ฐ ๊ฒฝ์ฐ๋ค์ ๋ชจ๋ ๋๋นํด์ผ ํด! ๐ก๏ธ
// ๊ฒฐ์ ์ทจ์ API
router.post('/payment/cancel', authenticateUser, async (req, res) => {
try {
const { orderId, reason } = req.body;
const order = await Order.findOne({
_id: orderId,
userId: req.user.id
});
if (!order) {
return res.status(404).json({
success: false,
message: '์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
if (order.status !== 'paid') {
return res.status(400).json({
success: false,
message: '์ทจ์ํ ์ ์๋ ์ฃผ๋ฌธ ์ํ์
๋๋ค'
});
}
// ์์ํฌํธ ์ก์ธ์ค ํ ํฐ ๋ฐ๊ธ
const getToken = await axios.post('https://api.iamport.kr/users/getToken', {
imp_key: process.env.IMP_API_KEY,
imp_secret: process.env.IMP_API_SECRET
});
const accessToken = getToken.data.response.access_token;
// ๊ฒฐ์ ์ทจ์ ์์ฒญ
const cancelResponse = await axios.post(
'https://api.iamport.kr/payments/cancel',
{
imp_uid: order.impUid,
amount: order.totalAmount,
reason: reason,
checksum: order.totalAmount // ์ทจ์ ๊ฐ๋ฅ ๊ธ์ก ๊ฒ์ฆ
},
{ headers: { 'Authorization': accessToken } }
);
if (cancelResponse.data.code === 0) {
// ์ทจ์ ์ฑ๊ณต
await Order.updateOne(
{ _id: order._id },
{
status: 'cancelled',
cancelledAt: new Date(),
cancelReason: reason
}
);
// ์ฌ๊ณ ๋ณต๊ตฌ
for (const item of order.items) {
await Product.updateOne(
{ _id: item.productId },
{ $inc: { stock: item.quantity } }
);
}
res.json({
success: true,
message: '์ฃผ๋ฌธ์ด ์ทจ์๋์์ต๋๋ค'
});
} else {
res.status(400).json({
success: false,
message: '๊ฒฐ์ ์ทจ์์ ์คํจํ์ต๋๋ค'
});
}
} catch (error) {
console.error('๊ฒฐ์ ์ทจ์ ์ค๋ฅ:', error);
res.status(500).json({
success: false,
message: '๊ฒฐ์ ์ทจ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
});
๐ฐ ๋ค์ํ ๊ฒฐ์ ์๋จ ์ง์
์์ฆ์ ์ ์ฉ์นด๋๋ง์ผ๋ก๋ ๋ถ์กฑํด! ์นด์นด์คํ์ด, ๋ค์ด๋ฒํ์ด, ํ ์ค, ํ์ด์ฝ ๋ฑ ๋ค์ํ ๊ฐํธ๊ฒฐ์ ๋ฅผ ์ง์ํด์ผ ํด. ํนํ ๋ชจ๋ฐ์ผ์์๋ ๊ฐํธ๊ฒฐ์ ๋น์ค์ด ์์ฒญ ๋๊ฑฐ๋ . ๐ฑ
1. ์ ์ฉ์นด๋ - ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๊ฒฐ์ ์๋จ. ํ ๋ถ ์ต์ ์ ๊ณต ๊ฐ๋ฅ
2. ๊ณ์ข์ด์ฒด - ์์๋ฃ๊ฐ ๋ฎ์์ ๊ณ ์ก ๊ฒฐ์ ์ ์ ๋ฆฌ
3. ๊ฐ์๊ณ์ข - ๋ฌดํต์ฅ ์ ๊ธ ๋ฐฉ์. ์ ๊ธ ํ์ธ ํ ์ฃผ๋ฌธ ์ฒ๋ฆฌ
4. ๊ฐํธ๊ฒฐ์ - ์นด์นด์คํ์ด, ๋ค์ด๋ฒํ์ด, ํ ์ค ๋ฑ. ์ฌ์ฉ์ ํธ์์ฑ ์ต๊ณ
5. ํด๋ํฐ ์์ก๊ฒฐ์ - ์์ก ๊ฒฐ์ ์ ์ ํฉ. ํต์ ์ฌ ์ฒญ๊ตฌ์์ ํฉ์ฐ
// ๊ฒฐ์ ์๋จ๋ณ ์ฒ๋ฆฌ ์์
const paymentData = {
pg: getPgProvider(paymentMethod), // PG์ฌ ์ ํ
pay_method: paymentMethod,
merchant_uid: generateMerchantUid(),
name: orderName,
amount: totalAmount,
buyer_email: buyerEmail,
buyer_name: buyerName,
buyer_tel: buyerTel,
buyer_addr: buyerAddr,
buyer_postcode: buyerPostcode
};
function getPgProvider(method) {
switch(method) {
case 'card':
return 'html5_inicis'; // ์ด๋์์ค
case 'trans':
return 'html5_inicis.INIBillTrs'; // ์ค์๊ฐ ๊ณ์ข์ด์ฒด
case 'vbank':
return 'html5_inicis'; // ๊ฐ์๊ณ์ข
case 'kakaopay':
return 'kakaopay'; // ์นด์นด์คํ์ด
case 'naverpay':
return 'naverpay'; // ๋ค์ด๋ฒํ์ด
case 'tosspay':
return 'tosspay'; // ํ ์คํ์ด
case 'phone':
return 'danal'; // ํด๋ํฐ ์์ก๊ฒฐ์
default:
return 'html5_inicis';
}
}
// ๊ฐ์๊ณ์ข์ ๊ฒฝ์ฐ ์
๊ธ ๋๊ธฐ ์ฒ๋ฆฌ
if (paymentMethod === 'vbank') {
paymentData.vbank_due = getVbankDueDate(); // ์
๊ธ ๊ธฐํ ์ค์
// ์นํ
์ผ๋ก ์
๊ธ ํ์ธ ํ ์ฃผ๋ฌธ ์ฒ๋ฆฌ
router.post('/payment/webhook', async (req, res) => {
const { imp_uid, merchant_uid, status } = req.body;
if (status === 'paid') {
// ๊ฐ์๊ณ์ข ์
๊ธ ์๋ฃ
await Order.updateOne(
{ merchantUid: merchant_uid },
{ status: 'paid', paidAt: new Date() }
);
// ์ฌ๊ณ ์ฐจ๊ฐ ๋ฐ ์ฃผ๋ฌธ ํ์ธ ๋ฉ์ผ ๋ฐ์ก
}
res.sendStatus(200);
});
}
๐ฆ ์ฃผ๋ฌธ ์์คํ ๊ตฌํํ๊ธฐ
๊ฒฐ์ ๊ฐ ์๋ฃ๋๋ฉด ์ด์ ์ฃผ๋ฌธ ์์คํ
์ด ์๋ํด์ผ ํด! ์ฃผ๋ฌธ ์์คํ
์ ๋จ์ํ "์ฃผ๋ฌธ ์๋ฃ" ์ํ๋ง ์ ์ฅํ๋ ๊ฒ ์๋๋ผ, ์ฃผ๋ฌธ ์๋ช
์ฃผ๊ธฐ ์ ์ฒด๋ฅผ ๊ด๋ฆฌํด์ผ ํด. ๐
๐ ์ฃผ๋ฌธ ์ํ ๊ด๋ฆฌ
์ฃผ๋ฌธ์ ์ฌ๋ฌ ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ์ ์งํ๋ผ. ๊ฐ ๋จ๊ณ๋ง๋ค ์ ์ ํ ์ฒ๋ฆฌ๊ฐ ํ์ํ์ง!
// ์ฃผ๋ฌธ ์คํค๋ง ์ ์
const orderSchema = new mongoose.Schema({
orderNumber: {
type: String,
required: true,
unique: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
items: [{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
productName: String,
quantity: Number,
price: Number,
options: Object // ์ํ ์ต์
(์์, ์ฌ์ด์ฆ ๋ฑ)
}],
totalAmount: {
type: Number,
required: true
},
shippingInfo: {
recipientName: String,
phone: String,
address: String,
postcode: String,
deliveryMessage: String
},
status: {
type: String,
enum: [
'pending', // ์ฃผ๋ฌธ ์ ์
'paid', // ๊ฒฐ์ ์๋ฃ
'preparing', // ์ํ ์ค๋น์ค
'shipping', // ๋ฐฐ์ก์ค
'delivered', // ๋ฐฐ์ก ์๋ฃ
'confirmed', // ๊ตฌ๋งค ํ์
'cancelled', // ์ฃผ๋ฌธ ์ทจ์
'refunded' // ํ๋ถ ์๋ฃ
],
default: 'pending'
},
paymentInfo: {
method: String,
impUid: String,
merchantUid: String,
paidAt: Date
},
shippingInfo: {
carrier: String, // ํ๋ฐฐ์ฌ
trackingNumber: String, // ์ด์ก์ฅ ๋ฒํธ
shippedAt: Date,
deliveredAt: Date
},
statusHistory: [{
status: String,
changedAt: {
type: Date,
default: Date.now
},
note: String
}],
createdAt: {
type: Date,
default: Date.now
}
});
// ์ฃผ๋ฌธ๋ฒํธ ์๋ ์์ฑ
orderSchema.pre('save', async function(next) {
if (this.isNew) {
const date = new Date();
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
const randomStr = Math.random().toString(36).substring(2, 8).toUpperCase();
this.orderNumber = `ORD${dateStr}${randomStr}`;
}
next();
});
๐ ๋ฐฐ์ก ์ถ์ ์์คํ
์ฃผ๋ฌธ์ด ์๋ฃ๋๋ฉด ์ฌ์ฉ์๋ "๋ด ๋ฌผ๊ฑด์ด ์ด๋์ฏค ์์๊น?" ๊ถ๊ธํดํ์์? ๊ทธ๋์ ๋ฐฐ์ก ์ถ์ ๊ธฐ๋ฅ์ด ํ์์ผ! ๐
๊ตญ๋ด ์ฃผ์ ํ๋ฐฐ์ฌ๋ค์ ๋๋ถ๋ถ ๋ฐฐ์ก ์กฐํ API๋ฅผ ์ ๊ณตํด. ์ค๋งํธํ๋ฐฐ(Sweet Tracker) ๊ฐ์ ํตํฉ ๋ฐฐ์ก ์กฐํ ์๋น์ค๋ฅผ ์ฐ๋ฉด ์ฌ๋ฌ ํ๋ฐฐ์ฌ๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํ ์ ์์ด์ ํธํด!
// Sweet Tracker API๋ฅผ ์ด์ฉํ ๋ฐฐ์ก ์กฐํ
const axios = require('axios');
const trackShipment = async (carrier, trackingNumber) => {
try {
const response = await axios.get(
'https://info.sweettracker.co.kr/api/v1/trackingInfo',
{
params: {
t_key: process.env.SWEET_TRACKER_API_KEY,
t_code: carrier, // ํ๋ฐฐ์ฌ ์ฝ๋
t_invoice: trackingNumber // ์ด์ก์ฅ ๋ฒํธ
}
}
);
return response.data;
} catch (error) {
console.error('๋ฐฐ์ก ์กฐํ ์คํจ:', error);
return null;
}
};
// ๋ฐฐ์ก ์กฐํ API
router.get('/order/:orderId/tracking', authenticateUser, async (req, res) => {
try {
const order = await Order.findOne({
_id: req.params.orderId,
userId: req.user.id
});
if (!order) {
return res.status(404).json({
success: false,
message: '์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
if (!order.shippingInfo.trackingNumber) {
return res.json({
success: true,
message: '์์ง ๋ฐฐ์ก์ด ์์๋์ง ์์์ต๋๋ค',
status: order.status
});
}
const trackingData = await trackShipment(
order.shippingInfo.carrier,
order.shippingInfo.trackingNumber
);
res.json({
success: true,
tracking: trackingData,
order: {
orderNumber: order.orderNumber,
status: order.status,
carrier: order.shippingInfo.carrier,
trackingNumber: order.shippingInfo.trackingNumber
}
});
} catch (error) {
console.error('๋ฐฐ์ก ์กฐํ ์ค๋ฅ:', error);
res.status(500).json({
success: false,
message: '๋ฐฐ์ก ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค'
});
}
});
// React Native ๋ฐฐ์ก ์ถ์ ํ๋ฉด
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, ActivityIndicator } from 'react-native';
const TrackingScreen = ({ route }) => {
const { orderId } = route.params;
const [tracking, setTracking] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadTracking();
}, []);
const loadTracking = async () => {
try {
const response = await fetch(
`https://api.example.com/order/${orderId}/tracking`,
{
headers: {
'Authorization': `Bearer ${userToken}`
}
}
);
const data = await response.json();
setTracking(data);
} catch (error) {
console.error('๋ฐฐ์ก ์กฐํ ์คํจ:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color="#6b9bd1" />
</View>
);
}
return (
<ScrollView style={{ flex: 1, padding: 16 }}>
<View style={{
backgroundColor: '#f8f9fb',
padding: 20,
borderRadius: 12,
marginBottom: 20
}}>
<Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
์ฃผ๋ฌธ๋ฒํธ: {tracking.order.orderNumber}
</Text>
<Text style={{ color: '#666', marginBottom: 4 }}>
ํ๋ฐฐ์ฌ: {tracking.order.carrier}
</Text>
<Text style={{ color: '#666' }}>
์ด์ก์ฅ๋ฒํธ: {tracking.order.trackingNumber}
</Text>
</View>
{tracking.tracking && tracking.tracking.trackingDetails && (
<View>
<Text style={{
fontSize: 16,
fontWeight: 'bold',
marginBottom: 16,
color: '#4a5568'
}}>
๋ฐฐ์ก ์งํ ์ํฉ
</Text>
{tracking.tracking.trackingDetails.map((detail, index) => (
<View
key={index}
style={{
flexDirection: 'row',
paddingVertical: 12,
borderLeftWidth: 2,
borderLeftColor: index === 0 ? '#6b9bd1' : '#e0e0e0',
paddingLeft: 16,
marginLeft: 8
}}
>
<View style={{ flex: 1 }}>
<Text style={{
fontSize: 14,
fontWeight: index === 0 ? 'bold' : 'normal',
color: index === 0 ? '#6b9bd1' : '#333',
marginBottom: 4
}}>
{detail.where}
</Text>
<Text style={{ fontSize: 13, color: '#666' }}>
{detail.kind}
</Text>
<Text style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
{detail.timeString}
</Text>
</View>
</View>
))}
</View>
)}
</ScrollView>
);
};
๐ ์ฃผ๋ฌธ ์๋ฆผ ์์คํ
์ฃผ๋ฌธ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์ฌ์ฉ์์๊ฒ ์๋ ค์ค์ผ ํด! ํธ์ ์๋ฆผ, ์ด๋ฉ์ผ, SMS ๋ฑ ๋ค์ํ ์ฑ๋๋ก ์๋ฆผ์ ๋ณด๋ผ ์ ์์ด. ๐ฌ
// Firebase Cloud Messaging์ ์ด์ฉํ ํธ์ ์๋ฆผ
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const sendOrderNotification = async (userId, orderId, status) => {
try {
const user = await User.findById(userId);
if (!user.fcmToken) {
console.log('FCM ํ ํฐ์ด ์์ต๋๋ค');
return;
}
const messages = {
'paid': {
title: '๊ฒฐ์ ๊ฐ ์๋ฃ๋์์ต๋๋ค ๐ณ',
body: '์ฃผ๋ฌธ์ด ์ ์์ ์ผ๋ก ์ ์๋์์ต๋๋ค.'
},
'preparing': {
title: '์ํ์ ์ค๋นํ๊ณ ์์ต๋๋ค ๐ฆ',
body: '๊ณง ๋ฐฐ์ก์ด ์์๋ฉ๋๋ค.'
},
'shipping': {
title: '๋ฐฐ์ก์ด ์์๋์์ต๋๋ค ๐',
body: '์ด์ก์ฅ ๋ฒํธ๋ก ๋ฐฐ์ก์ ์ถ์ ํ ์ ์์ต๋๋ค.'
},
'delivered': {
title: '๋ฐฐ์ก์ด ์๋ฃ๋์์ต๋๋ค โ
',
body: '์ํ์ ํ์ธํ์ ํ ๊ตฌ๋งคํ์ ๋ถํ๋๋ฆฝ๋๋ค.'
}
};
const message = messages[status];
if (!message) return;
await admin.messaging().send({
token: user.fcmToken,
notification: {
title: message.title,
body: message.body
},
data: {
type: 'order_status',
orderId: orderId.toString(),
status: status
},
android: {
priority: 'high'
},
apns: {
payload: {
aps: {
sound: 'default',
badge: 1
}
}
}
});
console.log('ํธ์ ์๋ฆผ ์ ์ก ์๋ฃ');
} catch (error) {
console.error('ํธ์ ์๋ฆผ ์ ์ก ์คํจ:', error);
}
};
// ์ฃผ๋ฌธ ์ํ ๋ณ๊ฒฝ ์ ์๋ฆผ ๋ฐ์ก
const updateOrderStatus = async (orderId, newStatus) => {
const order = await Order.findById(orderId);
order.status = newStatus;
order.statusHistory.push({
status: newStatus,
changedAt: new Date()
});
await order.save();
// ์๋ฆผ ๋ฐ์ก
await sendOrderNotification(order.userId, orderId, newStatus);
};
โก ์ฑ๋ฅ ์ต์ ํ ๋ฐ ๋ณด์
์ด์ปค๋จธ์ค ์ฑ์ ๋์ ์ ์์๊ฐ ๋ง์ ์ ์์ด์ ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ ๋ง ์ค์ํด! ํนํ ์ธ์ผ ๊ธฐ๊ฐ์ด๋ ํน๊ฐ ์ํ ์คํ ์๊ฐ์๋ ํธ๋ํฝ์ด ํญ๋ฐ์ ์ผ๋ก ์ฆ๊ฐํ๊ฑฐ๋ . ๐ฅ
๐ ๋์์ฑ ์ ์ด
์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ๊ฐ์ ์ํ์ ๊ตฌ๋งคํ๋ ค๊ณ ํ ๋ ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์ด. ์ฌ๊ณ ๊ฐ 1๊ฐ ๋จ์๋๋ฐ 10๋ช
์ด ๋์์ ์ฃผ๋ฌธํ๋ฉด? ๐ฑ ์ด๋ฐ ์ํฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ๋์์ฑ ์ ์ด๊ฐ ํ์ํด!
// Redis๋ฅผ ์ด์ฉํ ์ฌ๊ณ ๊ด๋ฆฌ
const redis = require('redis');
const client = redis.createClient();
// ์ฌ๊ณ ํ์ธ ๋ฐ ์ฐจ๊ฐ (์์์ ์ฐ์ฐ)
const decreaseStock = async (productId, quantity) => {
const key = `stock:${productId}`;
// Lua ์คํฌ๋ฆฝํธ๋ก ์์์ ์ฐ์ฐ ๋ณด์ฅ
const script = `
local stock = redis.call('GET', KEYS[1])
if not stock then
return -1
end
stock = tonumber(stock)
if stock < tonumber(ARGV[1]) then
return 0
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
`;
return new Promise((resolve, reject) => {
client.eval(script, 1, key, quantity, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
// ์ฃผ๋ฌธ ์์ฑ ์ ์ฌ๊ณ ํ์ธ
router.post('/order/create', authenticateUser, async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const { items } = req.body;
// 1. ์ฌ๊ณ ํ์ธ ๋ฐ ์ฐจ๊ฐ
for (const item of items) {
const result = await decreaseStock(item.productId, item.quantity);
if (result === -1) {
throw new Error('์ํ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
} else if (result === 0) {
throw new Error('์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค');
}
}
// 2. ์ฃผ๋ฌธ ์์ฑ
const order = new Order({
userId: req.user.id,
items: items,
totalAmount: calculateTotal(items),
status: 'pending'
});
await order.save({ session });
// 3. ํธ๋์ญ์
์ปค๋ฐ
await session.commitTransaction();
res.json({
success: true,
orderId: order._id,
orderNumber: order.orderNumber
});
} catch (error) {
// ํธ๋์ญ์
๋กค๋ฐฑ
await session.abortTransaction();
// ์ฌ๊ณ ๋ณต๊ตฌ
for (const item of items) {
await client.incrby(`stock:${item.productId}`, item.quantity);
}
res.status(400).json({
success: false,
message: error.message
});
} finally {
session.endSession();
}
});
๐ ๋ณด์ ๊ฐํ
์ด์ปค๋จธ์ค๋ ๊ฒฐ์ ์ ๋ณด๋ฅผ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ๋ณด์์ด ์๋ช
์ด์ผ! ๐ก๏ธ ๋ช ๊ฐ์ง ํ์ ๋ณด์ ์กฐ์น๋ค์ ์๋ ค์ค๊ฒ.
1. HTTPS ์ฌ์ฉ - ๋ชจ๋ ํต์ ์ ์ํธํํด์ผ ํด. Let's Encrypt๋ก ๋ฌด๋ฃ SSL ์ธ์ฆ์๋ฅผ ๋ฐ์ ์ ์์ด!
2. JWT ํ ํฐ ์ธ์ฆ - ์ฌ์ฉ์ ์ธ์ฆ์ JWT๋ฅผ ์ฌ์ฉํ๊ณ , ํ ํฐ ๋ง๋ฃ ์๊ฐ์ ์ ์ ํ ์ค์ ํด.
3. SQL Injection ๋ฐฉ์ง - ORM์ ์ฌ์ฉํ๊ฑฐ๋ Prepared Statement๋ฅผ ์จ์ ๋ฐฉ์งํด.
4. XSS ๋ฐฉ์ง - ์ฌ์ฉ์ ์ ๋ ฅ๊ฐ์ ํญ์ ๊ฒ์ฆํ๊ณ ์ด์ค์ผ์ดํ ์ฒ๋ฆฌํด.
5. CSRF ๋ฐฉ์ง - CSRF ํ ํฐ์ ์ฌ์ฉํด์ ์์กฐ ์์ฒญ์ ๋ง์.
6. Rate Limiting - API ํธ์ถ ํ์๋ฅผ ์ ํํด์ DDoS ๊ณต๊ฒฉ์ ๋ฐฉ์ดํด.
// JWT ์ธ์ฆ ๋ฏธ๋ค์จ์ด
const jwt = require('jsonwebtoken');
const authenticateUser = (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: '์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค'
});
}
return res.status(401).json({
success: false,
message: '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค'
});
}
};
// Rate Limiting
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100ํ ์์ฒญ
message: '๋๋ฌด ๋ง์ ์์ฒญ์ ๋ณด๋์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.',
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', apiLimiter);
// ๊ฒฐ์ API๋ ๋ ์๊ฒฉํ๊ฒ
const paymentLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1์๊ฐ
max: 10, // ์ต๋ 10ํ
message: '๊ฒฐ์ ์๋ ํ์๋ฅผ ์ด๊ณผํ์ต๋๋ค.'
});
app.use('/api/payment/', paymentLimiter);
๐ ์บ์ฑ ์ ๋ต
์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ ์บ์ฑํด์ DB ๋ถํ๋ฅผ ์ค์ฌ์ผ ํด! Redis๋ฅผ ํ์ฉํ๋ฉด ์์ฒญ๋ ์ฑ๋ฅ ํฅ์์ ๋ณผ ์ ์์ด. ๐
// Redis ์บ์ฑ ์์
const getProduct = async (productId) => {
const cacheKey = `product:${productId}`;
// 1. ์บ์ ํ์ธ
const cached = await client.get(cacheKey);
if (cached) {
console.log('์บ์ ํํธ!');
return JSON.parse(cached);
}
// 2. DB ์กฐํ
const product = await Product.findById(productId);
// 3. ์บ์ ์ ์ฅ (1์๊ฐ)
await client.setex(cacheKey, 3600, JSON.stringify(product));
return product;
};
// ์ํ ์ ๋ณด ๋ณ๊ฒฝ ์ ์บ์ ๋ฌดํจํ
const updateProduct = async (productId, updateData) => {
const product = await Product.findByIdAndUpdate(
productId,
updateData,
{ new: true }
);
// ์บ์ ์ญ์
await client.del(`product:${productId}`);
return product;
};
๐จ ์ฌ์ฉ์ ๊ฒฝํ(UX) ๊ฐ์
๊ธฐ๋ฅ์ด ์๋ฒฝํด๋ UX๊ฐ ๋ณ๋ก๋ฉด ์ฌ์ฉ์๋ค์ด ๋ ๋๋ฒ๋ ค. ๐ข ์ด์ปค๋จธ์ค์์ ํนํ ์ค์ํ UX ์์๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ!
๐๏ธ ์ฅ๋ฐ๊ตฌ๋ UX ํ
โข ๋น ๋ฅธ ๋ด๊ธฐ - ์ํ ์์ธ ํ์ด์ง์์ ๋ฐ๋ก ์ฅ๋ฐ๊ตฌ๋์ ๋ด์ ์ ์์ด์ผ ํด
โข ์๋ ์กฐ์ - +/- ๋ฒํผ์ผ๋ก ์ง๊ด์ ์ผ๋ก ์๋ ๋ณ๊ฒฝ
โข ์ต์ ์ ํ - ์์, ์ฌ์ด์ฆ ๋ฑ ์ต์ ์ ๋ช ํํ๊ฒ ํ์
โข ๊ฐ๊ฒฉ ํ์ - ๊ฐ๋ณ ๊ฐ๊ฒฉ๊ณผ ์ด ๊ฐ๊ฒฉ์ ๋ช ํํ๊ฒ
โข ์ฌ๊ณ ์๋ฆผ - ์ฌ๊ณ ๊ฐ ์ผ๋ง ์ ๋จ์์ผ๋ฉด ์๋ ค์ค์ ๊ตฌ๋งค ์๊ตฌ ์๊ทน
โข ์ ์ฅ ๊ธฐ๋ฅ - ๋์ค์ ๊ตฌ๋งคํ ์ํ์ ์์๋ฆฌ์คํธ๋ก ์ด๋
โข ๋น ๋ฅธ ์ญ์ - ์ค์์ดํ๋ก ๊ฐํธํ๊ฒ ์ญ์
๐ณ ๊ฒฐ์ UX ํ
โข ์ํด๋ฆญ ๊ฒฐ์ - ์์ฃผ ์ฐ๋ ๊ฒฐ์ ์๋จ ์ ์ฅ
โข ์งํ ์ํฉ ํ์ - ๊ฒฐ์ ๋จ๊ณ๋ฅผ ๋ช ํํ๊ฒ ๋ณด์ฌ์ค
โข ๋ฐฐ์ก์ง ์๋์์ฑ - ์ฃผ์ ๊ฒ์ API ํ์ฉ
โข ์ฟ ํฐ/ํฌ์ธํธ - ํ ์ธ ํํ์ ์ฝ๊ฒ ์ ์ฉํ ์ ์๊ฒ
โข ์ต์ข ํ์ธ - ๊ฒฐ์ ์ ์ฃผ๋ฌธ ๋ด์ญ ๋ค์ ํ์ธ
โข ์๋ฌ ์ฒ๋ฆฌ - ๊ฒฐ์ ์คํจ ์ ๋ช ํํ ์๋ด์ ์ฌ์๋ ์ต์
// ์ฃผ์ ๊ฒ์ API ์ฐ๋ (Daum ์ฐํธ๋ฒํธ ์๋น์ค)
import React, { useState } from 'react';
import { View, TouchableOpacity, Text, Modal } from 'react-native';
import { WebView } from 'react-native-webview';
const AddressSearchModal = ({ visible, onClose, onSelectAddress }) => {
const handleMessage = (event) => {
const data = JSON.parse(event.nativeEvent.data);
onSelectAddress({
postcode: data.zonecode,
address: data.address,
addressDetail: ''
});
onClose();
};
return (
<Modal visible={visible} animationType="slide">
<View style={{ flex: 1 }}>
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0'
}}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>
์ฃผ์ ๊ฒ์
</Text>
<TouchableOpacity onPress={onClose}>
<Text style={{ fontSize: 16, color: '#666' }}>๋ซ๊ธฐ</Text>
</TouchableOpacity>
</View>
<WebView
source={{
uri: 'https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js'
}}
onMessage={handleMessage}
injectedJavaScript={`
new daum.Postcode({
oncomplete: function(data) {
window.ReactNativeWebView.postMessage(JSON.stringify(data));
}
}).embed(document.body);
`}
/>
</View>
</Modal>
);
};
๐ฑ ๋ชจ๋ฐ์ผ ์ต์ ํ
๋ชจ๋ฐ์ผ ์ผํ์ด ๋์ธ์ธ ๋งํผ, ๋ชจ๋ฐ์ผ ์ต์ ํ๋ ํ์์ผ! ๐ฑ
โข ํฐ์น ์์ญ - ๋ฒํผ์ ์ต์ 44x44px ์ด์์ผ๋ก
โข ์คํฌ๋กค ์ฑ๋ฅ - FlatList, RecyclerView ๋ฑ ๊ฐ์ํ ๋ฆฌ์คํธ ์ฌ์ฉ
โข ์ด๋ฏธ์ง ์ต์ ํ - WebP ํฌ๋งท ์ฌ์ฉ, ๋ ์ด์ง ๋ก๋ฉ ์ ์ฉ
โข ์คํ๋ผ์ธ ์ง์ - ๋คํธ์ํฌ ๋๊ฒจ๋ ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ๋์ํ๊ฒ
โข ๋ก๋ฉ ์ํ - ์ค์ผ๋ ํค UI๋ก ๋ก๋ฉ ์ค์์ ํ์
โข ์ ์ค์ฒ - ์ค์์ดํ, ํํฌ๋ฆฌํ๋ ์ ๋ฑ ์ง๊ด์ ์ธ ์ ์ค์ฒ ์ง์
๐งช ํ ์คํธ ๋ฐ ๋๋ฒ๊น
์ด์ปค๋จธ์ค๋ ๋์ด ์ค๊ฐ๋ ์๋น์ค๋ผ์ ๋ฒ๊ทธ๊ฐ ์์ผ๋ฉด ์ ๋ง ํฐ์ผ ๋! ๐ฑ ์ฒ ์ ํ ํ
์คํธ๊ฐ ํ์์ผ.
โ ํ ์คํธ ์ ๋ต
// Jest๋ฅผ ์ด์ฉํ ๋จ์ ํ
์คํธ
describe('Cart Service', () => {
test('์ฅ๋ฐ๊ตฌ๋์ ์ํ ์ถ๊ฐ', async () => {
const userId = 'test-user-id';
const productId = 'test-product-id';
const quantity = 2;
const result = await addToCart(userId, productId, quantity);
expect(result.success).toBe(true);
expect(result.cart.items).toHaveLength(1);
expect(result.cart.items[0].quantity).toBe(2);
});
test('๋์ผ ์ํ ์ถ๊ฐ ์ ์๋ ์ฆ๊ฐ', async () => {
const userId = 'test-user-id';
const productId = 'test-product-id';
await addToCart(userId, productId, 1);
const result = await addToCart(userId, productId, 2);
expect(result.cart.items).toHaveLength(1);
expect(result.cart.items[0].quantity).toBe(3);
});
test('์ฌ๊ณ ๋ถ์กฑ ์ ์๋ฌ', async () => {
const userId = 'test-user-id';
const productId = 'out-of-stock-product';
const quantity = 100;
await expect(
addToCart(userId, productId, quantity)
).rejects.toThrow('์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค');
});
});
// ๊ฒฐ์ ํ
์คํธ
describe('Payment Service', () => {
test('๊ฒฐ์ ๊ธ์ก ๊ฒ์ฆ', async () => {
const order = {
merchantUid: 'test-order-123',
totalAmount: 50000
};
const paymentData = {
amount: 50000,
status: 'paid'
};
const result = await verifyPayment(order, paymentData);
expect(result.success).toBe(true);
});
test('๊ธ์ก ๋ถ์ผ์น ์ ์คํจ', async () => {
const order = {
merchantUid: 'test-order-123',
totalAmount: 50000
};
const paymentData = {
amount: 30000, // ๊ธ์ก ๋ถ์ผ์น
status: 'paid'
};
const result = await verifyPayment(order, paymentData);
expect(result.success).toBe(false);
expect(result.message).toContain('๊ธ์ก์ด ์ผ์นํ์ง ์์ต๋๋ค');
});
});
๐ ๋๋ฒ๊น ํ
1. ๊ฒฐ์ ๊ธ์ก ๋ถ์ผ์น
โ ์๋ฒ์์ ํญ์ ๊ธ์ก์ ์ฌ๊ณ์ฐํ๊ณ ๊ฒ์ฆํด์ผ ํด
2. ์ฌ๊ณ ๋์์ฑ ๋ฌธ์
โ Redis๋ DB ํธ๋์ญ์ ์ผ๋ก ์์์ ์ฐ์ฐ ๋ณด์ฅ
3. ๊ฒฐ์ ์ค๋ณต ์ฒ๋ฆฌ
โ ์ฃผ๋ฌธ๋ฒํธ๋ก ์ค๋ณต ์ฒดํฌ, ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ
4. ์ธ์ ๋ง๋ฃ
โ ํ ํฐ ๊ฐฑ์ ๋ก์ง ๊ตฌํ, ๋ฆฌํ๋ ์ ํ ํฐ ํ์ฉ
5. ๋คํธ์ํฌ ํ์์์
โ ์ ์ ํ ํ์์์ ์ค์ , ์ฌ์๋ ๋ก์ง ๊ตฌํ
// ๋ก๊น
์์คํ
๊ตฌ์ถ
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
})
]
});
// ์ฃผ๋ฌธ ์์ฑ ์ ๋ก๊น
router.post('/order/create', async (req, res) => {
const startTime = Date.now();
try {
logger.info('์ฃผ๋ฌธ ์์ฑ ์์', {
userId: req.user.id,
items: req.body.items,
totalAmount: req.body.totalAmount
});
const order = await createOrder(req.body);
logger.info('์ฃผ๋ฌธ ์์ฑ ์๋ฃ', {
orderId: order._id,
orderNumber: order.orderNumber,
duration: Date.now() - startTime
});
res.json({ success: true, order });
} catch (error) {
logger.error('์ฃผ๋ฌธ ์์ฑ ์คํจ', {
userId: req.user.id,
error: error.message,
stack: error.stack,
duration: Date.now() - startTime
});
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ์์ฑ์ ์คํจํ์ต๋๋ค'
});
}
});
๐ ๋ฐฐํฌ ๋ฐ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ๋ค๊ณ ๋์ด ์๋์ผ! ์ค์ ์๋น์ค๋ฅผ ์ด์ํ๋ฉด์ ๊ณ ๋ คํด์ผ ํ ๊ฒ๋ค์ด ๋ง์. ๐โโ๏ธ
โ๏ธ ํด๋ผ์ฐ๋ ์ธํ๋ผ
์์ฆ์ ๋๋ถ๋ถ ํด๋ผ์ฐ๋ ์๋น์ค๋ฅผ ์ฌ์ฉํด. AWS, GCP, Azure ๋ฑ์ด ์๋๋ฐ, ๊ฐ๊ฐ ์ฅ๋จ์ ์ด ์์ด.
| ์๋น์ค | ์ฅ์ | ๋จ์ |
|---|---|---|
| AWS | ๊ฐ์ฅ ๋ง์ ์๋น์ค, ํ๋ถํ ๋ ํผ๋ฐ์ค | ๋ณต์กํ ๊ฐ๊ฒฉ ์ ์ฑ , ๋ฌ๋ ์ปค๋ธ |
| GCP | ๋จธ์ ๋ฌ๋ ๊ฐ์ , ๊น๋ํ UI | ์๋์ ์ผ๋ก ์ ์ ๋ ํผ๋ฐ์ค |
| Azure | MS ์ํ๊ณ ์ฐ๋, ๊ธฐ์ ์นํ์ | ๊ฐ๋ฐ์ ๊ฒฝํ์ด ๋ค์ ๋ถ์กฑ |
| Naver Cloud | ๊ตญ๋ด ์๋น์ค, ํ๊ธ ์ง์ | ๊ธ๋ก๋ฒ ์๋น์ค ์ ํ์ |
๐ ๋ชจ๋ํฐ๋ง
์๋น์ค๊ฐ ์ ๋์๊ฐ๋์ง ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํด์ผ ํด! ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๋ฐ๋ก ์์์ฐจ๋ฆด ์ ์๋๋ก ๋ง์ด์ผ. ๐
// PM2๋ฅผ ์ด์ฉํ ํ๋ก์ธ์ค ๊ด๋ฆฌ
module.exports = {
apps: [{
name: 'ecommerce-api',
script: './server.js',
instances: 'max', // CPU ์ฝ์ด ์๋งํผ ์ธ์คํด์ค ์์ฑ
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
merge_logs: true,
// ์๋ ์ฌ์์
watch: false,
max_memory_restart: '1G',
// ๋ชจ๋ํฐ๋ง
monitoring: true
}]
};
// Sentry๋ฅผ ์ด์ฉํ ์๋ฌ ํธ๋ํน
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0
});
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());
๐ ํ์ต ๋ฆฌ์์ค ๋ฐ ์ปค๋ฎค๋ํฐ
์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ์ ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด? ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ์ถ์ฒํด! ๐
๊ณต์ ๋ฌธ์
โข React Native ๊ณต์ ๋ฌธ์
โข Flutter ๊ณต์ ๋ฌธ์
โข ๊ฐ PG์ฌ ๊ฐ๋ฐ์ ๋ฌธ์
์จ๋ผ์ธ ๊ฐ์
โข Udemy - React Native ์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ
โข ์ธํ๋ฐ - ์ค์ ์ผํ๋ชฐ ์ฑ ๋ง๋ค๊ธฐ
โข Nomad Coders - ํ์คํ ์ด์ปค๋จธ์ค
์ปค๋ฎค๋ํฐ
โข Stack Overflow - ๊ธฐ์ ์ง๋ฌธ
โข GitHub - ์คํ์์ค ํ๋ก์ ํธ
โข ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ(OKKY, ์ํ์ฝ๋ฉ ๋ฑ)
โข ์ฌ๋ฅ๋ท - ์ ๋ฌธ๊ฐ ๋ฉํ ๋ง ๋ฐ ํ๋ก์ ํธ ์๋ขฐ
๐ค ํ์ ๋๊ตฌ
ํ์ผ๋ก ๊ฐ๋ฐํ ๋ ํ์ํ ๋๊ตฌ๋ค๋ ์์๋๋ฉด ์ข์!
โข Git/GitHub - ๋ฒ์ ๊ด๋ฆฌ ๋ฐ ์ฝ๋ ๋ฆฌ๋ทฐ
โข Jira/Trello - ํ๋ก์ ํธ ๊ด๋ฆฌ
โข Slack/Discord - ํ ์ปค๋ฎค๋์ผ์ด์
โข Figma - ๋์์ธ ํ์
โข Postman - API ํ ์คํธ ๋ฐ ๋ฌธ์ํ
โข Notion - ๋ฌธ์ ๊ด๋ฆฌ
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ์ ์๊ฐ๋ณด๋ค ๊ณ ๋ คํด์ผ ํ ๊ฒ ์ ๋ง ๋ง์. ํ์ง๋ง ํ๋ํ๋ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ตฌํํด๋๊ฐ๋ค ๋ณด๋ฉด ์ด๋์ ์์ฑ๋ ๋์ ์ผํ๋ชฐ ์ฑ์ด ๋ง๋ค์ด์ง ๊ฑฐ์ผ! ๐ช
ํต์ฌ์ ๋ค์ ํ๋ฒ ์ ๋ฆฌํด๋ณผ๊ฒ:
1. ์ฅ๋ฐ๊ตฌ๋ - ๋ก์ปฌ/์๋ฒ/DB ์ ์ฅ ๋ฐฉ์์ ์ ์ ํ ์ ํํ๊ณ , ์ฌ์ฉ์ ๊ฒฝํ์ ์ต์ฐ์ ์ผ๋ก ์๊ฐํด
2. ๊ฒฐ์ - PG์ฌ API๋ฅผ ์์ ํ๊ฒ ์ฐ๋ํ๊ณ , ๋ฐ๋์ ์๋ฒ์์ ๊ฒ์ฆํด. ๋ณด์์ด ์๋ช
์ด์ผ!
3. ์ฃผ๋ฌธ - ์ฃผ๋ฌธ ์๋ช
์ฃผ๊ธฐ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ์ฌ์ฉ์์๊ฒ ์ ์ ํ ์๋ฆผ์ ๋ณด๋ด์ค
4. ์ฑ๋ฅ - ๋์์ฑ ์ ์ด, ์บ์ฑ, ์ต์ ํ๋ก ์์ ์ ์ธ ์๋น์ค๋ฅผ ๋ง๋ค์ด
5. ๋ณด์ - HTTPS, JWT, Rate Limiting ๋ฑ์ผ๋ก ์์คํ
์ ๋ณดํธํด
์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ง๋ง, ์ค์ ๋ก ๋ง๋ค์ด๋ณด๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ๊ธธ์ด์ผ. ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋๊ฐ๋ณด์! ๐
๊ทธ๋ฆฌ๊ณ ํผ์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์๋ค์๊ฒ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๋๋ก๋ ์ ๋ฌธ๊ฐ์ ํ๋ง๋ ์กฐ์ธ์ด ๋ฉฐ์น ๊ฐ์ ์ฝ์ง์ ์ค์ฌ์ฃผ๊ธฐ๋ ํ๊ฑฐ๋ ! ๐
์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ, ์ด๋ ต์ง๋ง ์ ๋ง ๋ณด๋์ฐฌ ์์
์ด์ผ. ๋ด๊ฐ ๋ง๋ ์ฑ์ผ๋ก ์ฌ๋๋ค์ด ์ผํํ๊ณ , ํ๋งค์๋ ์์ต์ ์ฌ๋ฆฌ๊ณ , ๊ตฌ๋งค์๋ ํธ๋ฆฌํ๊ฒ ๋ฌผ๊ฑด์ ๋ฐ๋ ๊ฑธ ๋ณด๋ฉด ๋ฟ๋ฏํจ์ด ์ฅ๋ ์๋๊ฑฐ๋ ! ๐
์, ์ด์ ๋น์ ๋ ์ด์ปค๋จธ์ค ์ฑ ๊ฐ๋ฐ์๊ฐ ๋ ์ค๋น๊ฐ ๋์ด! ํ์ดํ
! ๐
์ด ๊ธ์ด ๋์์ด ๋์๋ค๋ฉด, ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด๋ฉด์
๋ ๊น์ด ์๋ ํ์ต์ ์ด์ด๊ฐ๋ณด์ธ์! ๐
Happy Coding! ๐ปโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.