자바스크립트에서 Class를 추구하면 안될까?
React.js 이전의 Javascript에서는
DOM에 직접 접근했어요
절차지향적으로 작성했어요
명령형으로 작성했어요
...
아주아주 간단한 쇼핑몰
Item |
Price |
Quantity |
Total |
|
Total: |
$0.00 |
|
절차지향적 코드
const products = [
{ id: 1, name: 'Product 1', price: 10.00 },
{ id: 2, name: 'Product 2', price: 15.00 },
{ id: 3, name: 'Product 3', price: 20.00 }
];
let cart = [];
function updateCartTable() {
const cartItemsElement = document.getElementById('cart-items');
cartItemsElement.innerHTML = '';
let totalPrice = 0;
for (let i = 0; i < cart.length; i++) {
const item=cart[i];
const product=products.find(p=> p.id === item.product.id);
const totalItemPrice = product.price * item.quantity;
totalPrice += totalItemPrice;
const row = document.createElement('tr');
const removeButton = document.createElement('button');
removeButton.textContent = 'Remove';
removeButton.addEventListener('click', () => removeFromCart(product));
row.innerHTML = `
${product.name} |
${product.price.toFixed(2)} |
${item.product.quantity} |
${totalItemPrice.toFixed(2)} |
|
`;
row.lastElementChild.appendChild(removeButton);
cartItemsElement.appendChild(row);
}
document.getElementById('total-price').textContent = `${totalPrice.toFixed(2)}`;
}
function addToCart(product, quantity) {
const existingItem = cart.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
cart.push({ product, quantity: 1 });
}
updateCartTable();
}
function removeFromCart(product) {
const index = cart.findIndex(item => item.product.id === product.id);
if (index !== -1) {
cart.splice(index, 1);
updateCartTable();
}
}
const productListElement = document.getElementById('product-list');
for (const product of products) {
const productItem = document.createElement('div');
productItem.classList.add('product-item');
const productName = document.createElement('h3');
productName.textContent = product.name;
const productPrice = document.createElement('p');
productPrice.textContent = `Price: $${product.price.toFixed(2)}`;
const addToCartButton = document.createElement('button');
addToCartButton.textContent = 'Add to Cart';
addToCartButton.addEventListener('click', () => addToCart(product, 1));
productItem.appendChild(productName);
productItem.appendChild(productPrice);
productItem.appendChild(addToCartButton);
productListElement.appendChild(productItem);
}
이런 점이 아쉬워요
함수명 선언할 때 반복이 있어요
같은 파일 내의 누구나 변수에 접근할 수 있어요
책임이 명확하지 않아 디버깅이 어려워질 수 있어요
프로토타입 활용
function Product(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
function Cart() {
this.items = [];
}
Cart.prototype.addItem = function(product, quantity) {
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
this.updateTable();
};
Cart.prototype.removeItem = function(product) {
const index = this.items.findIndex(item => item.product.id === product.id);
if (index !== -1) {
this.items.splice(index, 1);
this.updateTable();
}
};
Cart.prototype.updateTable = function() {
const tableBody = document.getElementById('cart-table-body');
tableBody.innerHTML = '';
for (const { product, quantity } of this.items) {
const row = document.createElement('tr');
const removeButton = document.createElement('button');
removeButton.textContent = 'Remove';
removeButton.addEventListener('click', () => this.removeItem(product));
row.innerHTML = `
${product.name} |
${quantity} |
${product.price * quantity} |
|
`;
row.lastElementChild.appendChild(removeButton);
tableBody.appendChild(row);
}
};
function ProductListRenderer(cart, products) {
this.cart = cart;
this.products = products;
this.productListElement = document.getElementById('product-list');
}
ProductListRenderer.prototype.renderProducts = function() {
for (const product of this.products) {
const productItem = this.createProductItem(product);
this.productListElement.appendChild(productItem);
}
};
ProductListRenderer.prototype.createProductItem = function(product) {
const productItem = document.createElement('div');
productItem.classList.add('product-item');
const productName = document.createElement('h3');
productName.textContent = product.name;
const productPrice = document.createElement('p');
productPrice.textContent = `Price: $${product.price.toFixed(2)}`;
const addToCartButton = document.createElement('button');
addToCartButton.textContent = 'Add to Cart';
addToCartButton.addEventListener('click', () => this.cart.addItem(product, 1));
productItem.appendChild(productName);
productItem.appendChild(productPrice);
productItem.appendChild(addToCartButton);
return productItem;
};
const products = [
new Product(1, 'Product 1', 10.99),
new Product(2, 'Product 2', 15.99),
new Product(3, 'Product 3', 20.99),
];
const cart = new Cart();
const productListRenderer = new ProductListRenderer(cart, products);
productListRenderer.renderProducts();
또 아쉬운 점이 있네요!
선언부에 반복이 있어요
어디서든 메서드를 선언할 수 있어요
응집도가 낮아질 수 있어요
Class
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class Cart {
constructor() {
this.items = [];
}
addItem(product, quantity) {
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
this.updateCartTable();
}
removeItem(product) {
const index = this.items.findIndex(item => item.product.id === product.id);
if (index !== -1) {
this.items.splice(index, 1);
this.updateCartTable();
}
}
updateCartTable() {
const tableBody = document.getElementById('cart-table-body');
tableBody.innerHTML = '';
for (const { product, quantity } of this.items) {
const row = document.createElement('tr');
const removeButton = document.createElement('button');
removeButton.textContent = 'Remove';
removeButton.addEventListener('click', () => removeFromCart(product));
row.innerHTML = `
${product.name} |
${quantity} |
${product.price * quantity} |
|
`;
row.lastElementChild.appendChild(removeButton);
tableBody.appendChild(row);
}
}
}
class ProductListRenderer {
constructor(cart, products) {
this.cart = cart;
this.products = products;
this.productListElement = document.getElementById('product-list');
}
renderProducts() {
for (const product of this.products) {
const productItem = this.createProductItem(product);
this.productListElement.appendChild(productItem);
}
}
createProductItem(product) {
const productItem = document.createElement('div');
productItem.classList.add('product-item');
const productName = document.createElement('h3');
productName.textContent = product.name;
const productPrice = document.createElement('p');
productPrice.textContent = `Price: $${product.price.toFixed(2)}`;
const addToCartButton = document.createElement('button');
addToCartButton.textContent = 'Add to Cart';
addToCartButton.addEventListener('click', () => this.cart.addItem(product, 1));
productItem.appendChild(productName);
productItem.appendChild(productPrice);
productItem.appendChild(addToCartButton);
return productItem;
}
}
const products = [
new Product(1, 'Product 1', 10.99),
new Product(2, 'Product 2', 15.99),
new Product(3, 'Product 3', 20.99),
];
const cart = new Cart();
const productListRenderer = new ProductListRenderer(cart, products);
productListRenderer.renderProducts();
명령형 코드에 전반적으로 아쉬운 점이 있어요
코드를 재사용하기가 어려워요
DOM을 직접 조작하는 과정에서 코드가 복잡해져요
변수의 상태를 관리하기 어려워요
React.js Class 컴포넌트
import React, { Component } from 'react';
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class ProductListItem extends Component {
handleAddToCart = () => {
this.props.onAddToCart(this.props.product, 1);
}
render() {
const { product } = this.props;
return (
{product.name}
Price: ${product.price.toFixed(2)}
);
}
}
class ProductList extends Component {
render() {
const { products, onAddToCart } = this.props;
return (
{products.map(product => (
))}
);
}
}
class Cart extends Component {
render() {
const { items, onRemoveItem } = this.props;
return (
Product |
Quantity |
Total |
Action |
{items.map(({ product, quantity }) => (
{product.name} |
{quantity} |
{product.price * quantity} |
|
))}
);
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
products: [
new Product(1, 'Product 1', 10.99),
new Product(2, 'Product 2', 15.99),
new Product(3, 'Product 3', 20.99),
],
cart: {
items: []
}
};
}
handleAddToCart = (product, quantity) => {
const existingItem = this.state.cart.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.setState(prevState => ({
cart: {
items: [...prevState.cart.items, { product, quantity }]
}
}));
}
}
handleRemoveItem = (product) => {
this.setState(prevState => ({
cart: {
items: prevState.cart.items.filter(item => item.product.id !== product.id)
}
}));
}
render() {
return (
);
}
}
export default App;
클래스 컴포넌트 특유의 보일러플레이트 코드가 생겼어요!
React.js Function 컴포넌트
import React, { useState } from 'react';
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
const ProductListItem = ({ product, onAddToCart }) => {
const handleAddToCart = () => {
onAddToCart(product, 1);
};
return (
{product.name}
Price: ${product.price.toFixed(2)}
);
};
const ProductList = ({ products, onAddToCart }) => {
return (
{products.map(product => (
))}
);
};
const Cart = ({ items, onRemoveItem }) => {
return (
Product |
Quantity |
Total |
Action |
{items.map(({ product, quantity }) => (
{product.name} |
{quantity} |
{product.price * quantity} |
|
))}
);
};
const App = () => {
const [products, setProducts] = useState([
new Product(1, 'Product 1', 10.99),
new Product(2, 'Product 2', 15.99),
new Product(3, 'Product 3', 20.99),
]);
const [cart, setCart] = useState({
items: [],
});
const handleAddToCart = (product, quantity) => {
const existingItem = cart.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
setCart(prevState => ({
items: [...prevState.items],
}));
} else {
setCart(prevState => ({
items: [...prevState.items, { product, quantity }],
}));
}
};
const handleRemoveItem = product => {
setCart(prevState => ({
items: prevState.items.filter(item => item.product.id !== product.id),
}));
};
return (
);
};
export default App;
좀 더 살펴보면은...
부모 컴포넌트에 상태와 로직이 종속적이에요
상품 정보나 장바구니 정보를 사용하는 곳에서 재사용하기가 어려워졌어요
useSyncExternalStore
class ProductStore {
static instance = null;
constructor() {
if (ProductStore.instance) {
return ProductStore.instance;
}
this.products = [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 },
{ id: 3, name: 'Product 3', price: 30 },
];
this.cartItems = [];
this.subscribers = new Set();
ProductStore.instance = this;
}
getProducts() {
return this.products;
}
getCartItems() {
return this.cartItems;
}
addToCart(product, quantity) {
const existingItem = this.cartItems.find((item) => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.cartItems.push({ product, quantity });
}
this.notifySubscribers();
}
removeFromCart(product) {
const itemIndex = this.cartItems.findIndex((item) => item.product.id === product.id);
if (itemIndex !== -1) {
this.cartItems.splice(itemIndex, 1);
this.notifySubscribers();
}
}
subscribe(callback) {
this.subscribers.add(callback);
return () => this.unsubscribe(callback);
}
unsubscribe(callback) {
this.subscribers.delete(callback);
}
notifySubscribers() {
this.subscribers.forEach((subscriber) => subscriber());
}
}
const ProductList = () => {
const store = new ProductStore();
const products = useSyncExternalStore(store.subscribe, store.getProducts);
const handleAddToCart = (product, quantity) => {
store.addToCart(product, quantity);
};
return (
{products.map((product) => (
))}
);
};
const ProductListItem = ({ product, onAddToCart }) => {
const handleAddToCart = () => {
onAddToCart(product, 1);
};
return (
{product.name}
Price: ${product.price}
);
};
const Cart = () => {
const store = new ProductStore();
const cartItems = useSyncExternalStore(store.subscribe, store.getCartItems);
const handleRemoveFromCart = (product) => {
store.removeFromCart(product);
};
return (
Product |
Quantity |
Price |
Actions |
{cartItems.map((item, index) => (
{item.product.name} |
{item.quantity} |
{item.product.price * item.quantity} |
|
))}
);
};
const App = () => {
return (
<>
>
);
};
export default App;
구독을 관리하는 부분에 보일러플레이트 코드가 생겼네요!
Valtio
import { proxy, useSnapshot } from 'valtio';
class ProductStore {
static instance = null;
state = proxy({
products: [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 },
{ id: 3, name: 'Product 3', price: 30 },
],
cartItems: [],
});
static getInstance() {
if (!ProductStore.instance) {
ProductStore.instance = new ProductStore();
}
return ProductStore.instance;
}
addToCart(product, quantity) {
const existingItem = this.state.cartItems.find((item) => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.state.cartItems.push({ product, quantity });
}
}
removeFromCart(product) {
const itemIndex = this.state.cartItems.findIndex((item) => item.product.id === product.id);
if (itemIndex !== -1) {
this.state.cartItems.splice(itemIndex, 1);
}
}
}
const ProductList = () => {
const store = ProductStore.getInstance();
const { products } = useSnapshot(store.state);
const handleAddToCart = (product, quantity) => {
store.addToCart(product, quantity);
};
return (
{products.map((product) => (
))}
);
};
const ProductListItem = ({ product, onAddToCart }) => {
const handleAddToCart = () => {
onAddToCart(product, 1);
};
return (
{product.name}
Price: ${product.price}
);
};
const Cart = () => {
const store = ProductStore.getInstance();
const { cartItems } = useSnapshot(store.state);
const handleRemoveFromCart = (product) => {
store.removeFromCart(product);
};
return (
Product |
Quantity |
Price |
Actions |
{cartItems.map((item, index) => (
{item.product.name} |
{item.quantity} |
{item.product.price * item.quantity} |
|
))}
);
};
const App = () => {
return (
<>
>
);
};
export default App;
함수명 선언할 때 반복이 있어요
같은 파일 내의 누구나 변수에 접근할 수 있어요
책임이 명확하지 않아 디버깅이 어려워질 수 있어요
절차 지향 -> 객체 지향
선언부에 반복이 있어요
어디서든 메서드를 선언할 수 있어요
응집도가 낮아질 수 있어요
명령형 -> 선언형
부모 컴포넌트에 상태와 로직이 종속적이에요
다시 재사용하기가 어려워졌어요
내부상태 -> 전역상태