이번 프로젝트를 React로 하게 될 것 같아서 Vue의 개념과 매핑되는 React 개념을 정리해보겠습니다.
Vue3
src/
├── components/
│ ├── Counter.vue
│ ├── <template> (HTML)
│ ├── <script> (JavaScript)
│ └── <style> (CSS)
└── App.vue
React
src/
├── components/
│ ├── Counter.jsx // 컴포넌트 로직
│ └── Counter.module.css // 스타일
└── App.jsx
Vue3는 SFC (Single File Component) 라서 한 파일에 HTML, JavaScript, CSS 가 다 들어있는 반면, React는 css는 따로 분리되는 모습
.css 앞에 붙어있는 .module은 Vue의 scoped 역할을 해서 해당 컴포넌트에만 영향을 줄 수 있도록 한다.
Counter.vue
<template>
<div class="counter">
<h2>{{ title }}</h2>
<p class="count">{{ count }}</p>
<button @click="increment" class="btn">증가</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const title = '카운터'
const increment = () => {
count.value++
}
</script>
<style scoped>
.counter {
padding: 20px;
border: 1px solid #ccc;
}
.count {
font-size: 24px;
font-weight: bold;
}
.btn {
background: blue;
color: white;
padding: 10px;
}
</style>
Counter.jsx
import React, { useState } from 'react';
import styles from './Counter.module.css';
function Counter() {
const [count, setCount] = useState(0);
const title = '카운터';
const increment = () => {
setCount(count + 1);
};
return (
<div className={styles.counter}>
<h2>{title}</h2>
<p className={styles.count}>{count}</p>
<button onClick={increment} className={styles.btn}>
증가
</button>
</div>
);
}
export default Counter;
Counter.module.css
.counter {
padding: 20px;
border: 1px solid #ccc;
}
.count {
font-size: 24px;
font-weight: bold;
}
.btn {
background: blue;
color: white;
padding: 10px;
}
// Vue 3
const count = ref(0)
count.value = 10 // 값 변경
// React
const [count, setCount] = useState(0)
setCount(10) // 값 변경
// Vue 3
const user = reactive({
name: 'John',
age: 25
})
user.name = 'Jane' // 직접 변경 가능
// React
const [user, setUser] = useState({
name: 'John',
age: 25
})
setUser({...user, name: 'Jane'}) // 불변성 유지
// Vue 3
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
// React
const [count, setCount] = useState(0)
const doubleCount = useMemo(() => count * 2, [count])
// Vue 3
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} -> ${newVal}`)
})
// React
useEffect(() => {
console.log('count changed:', count)
}, [count])
// Vue 3
onMounted(() => {
console.log('컴포넌트가 마운트됨')
})
// React
useEffect(() => {
console.log('컴포넌트가 마운트됨')
}, []) // 빈 배열로 한 번만 실행
// Vue 3
onUnmounted(() => {
console.log('컴포넌트가 언마운트됨')
})
// React
useEffect(() => {
return () => {
console.log('컴포넌트가 언마운트됨')
}
}, []) // cleanup 함수 반환
// Vue 3
watchEffect(() => {
console.log('count:', count.value)
console.log('name:', name.value)
// count나 name이 변경되면 자동 실행
})
// React
useEffect(() => {
console.log('count:', count)
console.log('name:', name)
}, [count, name]) // 의존성 배열 명시 필요
<template>
<div>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="할 일 입력">
<ul>
<li v-for="todo in todos" :key="todo.id">
<span :class="{ completed: todo.done }" @click="toggleTodo(todo.id)">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)">삭제</button>
</li>
</ul>
<p v-if="todos.length === 0">할 일이 없습니다.</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newTodo = ref('')
const todos = ref([])
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
done: false
})
newTodo.value = ''
}
}
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
import React, { useState } from 'react';
function TodoList() {
const [newTodo, setNewTodo] = useState('');
const [todos, setTodos] = useState([]);
const addTodo = () => {
if (newTodo.trim()) {
setTodos([...todos, {
id: Date.now(),
text: newTodo,
done: false
}]);
setNewTodo('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
addTodo();
}
};
return (
<div>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="할 일 입력"
/>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
className={todo.done ? 'completed' : ''}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
{todos.length === 0 && <p>할 일이 없습니다.</p>}
</div>
);
}
export default TodoList;
vue, react 둘 다 javascript의 프레임워크로써 비슷하게 작동하지만 하나 큰 차이점이 있는데, 바로 Vue는 가변성이고, React는 불변성이란 것이다.
그래서 react에서는 set으로 새로운 객체를 생성해서 덮어쓰기 해준다.
import React, { useState } from 'react'
function App() {
const [user, setUser] = useState({
name: '홍길동',
age: 25
})
const [items, setItems] = useState(['사과', '바나나'])
const updateData = () => {
// 직접 수정하면 안 됨
// user.name = '김철수' // React가 변화 감지 못함
// items.push('오렌지') // React가 변화 감지 못함
// 새로운 객체/배열 생성
setUser({
...user, // 기존 데이터 복사
name: '김철수', // 새로운 값
age: 30
})
setItems([
...items, // 기존 배열 복사
'오렌지' // 새로운 요소 추가
])
}
return (
<div>
<p>이름: {user.name}</p>
<p>나이: {user.age}</p>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)
}
그래서 Vue가 조금 직관적이고 쉽지만 예측하기 어렵고 디버깅이 어려울 수 있지만, React는 반대로 살짝 복잡하지만 디버깅이 용이하다.
대세는 리액트인가요