리액트 개발자 경험이 좋다고 생각하시나요?
가끔 Meta와 Vercel에 휘둘리고 있다고 생각이 들지 않나요.
다른 언어와 간단히 비교해보고 시야를 넓히고자 다른 프레임워크를 조사했습니다.
우선 Java 코드와 React 코드를 간단히 비교해보겠습니다.
다음과 같은 비즈니스 로직이 필요하다고 가정
@Service
public class DashboardService {
public DashboardData getDashboard(DashboardRequest request) {
// 1. 권한에 따른 차트 필터링
List<Chart> allowedCharts = chartRepository
.findByUserPermissions(request.getUserId());
// 2. 선택된 타입으로 필터링
List<Chart> filteredCharts = allowedCharts.stream()
.filter(chart -> chart.getType().equals(request.getChartType()))
.collect(toList());
// 3. 데이터 변환 및 가공
List<ChartData> processedData = filteredCharts.stream()
.map(chart -> dataProcessor.process(chart, request.getFilters()))
.collect(toList());
// 4. 메트릭 계산
List<ChartMetrics> metrics = processedData.stream()
.map(data -> metricsCalculator.calculate(data))
.collect(toList());
// 5. 집계
DashboardSummary summary = summaryAggregator.aggregate(metrics);
return new DashboardData(processedData, summary);
}
}
Java Service Layer에서는 직관적이고 명확하게 구현가능
@RestController
public class DashboardController {
@GetMapping("/dashboard")
public DashboardData getDashboard(
@RequestParam String chartType,
@RequestParam String theme,
@RequestParam Map<String, String> filters
) {
return dashboardService.getDashboard(chartType, theme, filters);
}
const DashboardComponent = () => {
const [state, setState] = useState({
user: { id: '123', permissions: [...] },
ui: { selectedChartType: 'line' },
data: { charts: [...], filters: {...} }
});
// 1. 권한에 따른 차트 필터링
const allowedCharts = useMemo(() => {
return state.data.charts.filter(chart =>
state.user.permissions.includes(chart.requiredPermission)
);
}, [state.data.charts, state.user.permissions]);
// 2. 선택된 타입으로 필터링
const filteredCharts = useMemo(() => {
return allowedCharts.filter(chart =>
chart.config.type === state.ui.selectedChartType
);
}, [allowedCharts, state.ui.selectedChartType]);
// 3. 데이터 변환 및 가공
const processedData = useMemo(() => {
return filteredCharts.map(chart => ({
...chart,
data: dataProcessor.process(chart.rawData, state.data.filters)
}));
}, [filteredCharts, state.data.filters]);
// 4. 메트릭 계산
const chartMetrics = useMemo(() => {
return processedData.map(chart => ({
id: chart.id,
metrics: metricsCalculator.calculate(chart.data)
}));
}, [processedData]);
// 5. 집계
const dashboardSummary = useMemo(() => {
return summaryAggregator.aggregate(
chartMetrics.map(item => item.metrics)
);
}, [chartMetrics]);
return (
<div>
{/* UI 렌더링 */}
</div>
);
};
Java Stateless 방식은 사용자 경험보다는 개발자 경험에 직관적이도록 초점이 맞춰짐
Java는 "단순함"을 선택했고, React는 "사용자 경험"을 선택
자동 리액티브 환경에서는 어쩔 수 없는걸까?
그런데 Svelte는 이 두 가지를 모두 잡으려고 한다.
컴파일 타임에 최적화해서 성능 확보?
React 19에서 다루는 문제 아닌가요
React 19: 기존 패러다임 유지 + 자동화
여전히 남는 오버헤드
React 19 컴파일러는 useMemo를 자동화했지만,
근본적인 패러다임(불변성, Virtual DOM)은 그대로
Svelte: 패러다임 자체 변경
핵심 아이디어
런타임이 아닌 빌드타임에 모든 것을 결정하자
그렇다면 컴파일 타임에 최적화된 코드를 빌드에 새로 생성하는데 번들링 사이즈가 왜 줄어들까?
비트마스크란?
// 전통적인 방법 O(n)
const changedVariables = ['count', 'name', 'age'];
function checkIfChanged(variableName) {
// 배열을 순회해서 찾기 (최악의 경우 n번 비교)
for (let i = 0; i < changedVariables.length; i++) {
if (changedVariables[i] === variableName) {
return true;
}
}
return false;
}
if (checkIfChanged('count')) updateCountDOM(); // O(n)
if (checkIfChanged('name')) updateNameDOM(); // O(n)
if (checkIfChanged('age')) updateAgeDOM(); // O(n)
// Svelte 비트마스크 방법 O(1)
// 각 변수에 고유한 숫자(비트 위치) 할당
const VARIABLE_BITS = {
count: 1, // 이진수: 0001
name: 2, // 이진수: 0010
age: 4, // 이진수: 0100
email: 8 // 이진수: 1000
};
// dirty는 "어떤 변수들이 바뀌었는지" 기록하는 메모장
let dirty = 0; // 초기값: 0000 (아무것도 안 바뀜)
// 변수 변경 표시
function markDirty(variable) {
dirty |= VARIABLE_BITS[variable]; // OR 연산으로 비트 켜기
}
// 변경 확인
function isChanged(variable) {
return (dirty & VARIABLE_BITS[variable]) !== 0; // AND 연산으로 비트 확인
}
// 사용
markDirty('count'); // dirty = 0001
markDirty('name'); // dirty = 0011 (count + name)
if (dirty & /*count*/ 1) updateCountDOM(); // O(1) - 1 CPU 사이클
if (dirty & /*name*/ 2) updateNameDOM(); // O(1) - 1 CPU 사이클
if (dirty & /*age*/ 4) updateAgeDOM(); // O(1) - 1 CPU 사이클
// 성능 테스트: 1000개 변수 중 변경된 것 찾기
// 전통적 방법 (React 스타일)
console.time('traditional');
const changes = ['var1', 'var500', 'var999'];
for (let i = 0; i < 1000; i++) {
const varName = `var${i}`;
if (changes.includes(varName)) { // O(n) 매번 배열 순회
updateDOM(varName);
}
}
console.timeEnd('traditional');
// 결과: 15.2ms
// 비트마스크 방법 (Svelte 스타일)
console.time('bitmask');
let dirty = (1 << 1) | (1 << 500) | (1 << 999); // 비트 설정
for (let i = 0; i < 1000; i++) {
if (dirty & (1 << i)) { // O(1) 비트 연산
updateDOM(`var${i}`);
}
}
console.timeEnd('bitmask');
// 결과: 0.8ms
Svelte도 완벽하지 않고 결국 대규모 앱에서는 번들링 사이즈가 오히려 클 수 있고
기본적으로 빌드 타임은 더 길다고 합니다.
치명적으로 학습곡선과 생태계의 문제가 심각하죠.
그래도 조사에 얻어 가는 게 있어야 하니
여기서 리액트 개발 할 때 적용 할만한 내용을 고민 해 봤습니다.
당연한 내용 일 수 있지만
Svelte의 철학을 통해 '원래 알던 좋은 습관'의 중요성을 다시 한번 깨닫고
React 환경에서 이를 의식적으로 실천하자는 취지에서 작성
// ❌ React 안티패턴: 모든 것을 상태로 (O(n) 증가)
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0); // 계산 가능
const [name, setName] = useState('');
const [greeting, setGreeting] = useState(''); // 계산 가능
const [summary, setSummary] = useState(''); // 계산 가능
// 상태 동기화 지옥: O(5) 체크 + 수동 동기화
useEffect(() => { setDoubled(count * 2); }, [count]);
useEffect(() => { setGreeting(`Hello ${name}`); }, [name]);
useEffect(() => { setSummary(`${greeting}, doubled: ${doubled}`); }, [greeting, doubled]);
// ✅ Svelte 패러다임을 React에 적용: O(2) 체크
const [count, setCount] = useState(0); // 기본 상태
const [name, setName] = useState(''); // 기본 상태
// 컴파일러가 자동 계산하듯 useMemo로 자동 계산
const doubled = useMemo(() => count * 2, [count]);
const greeting = useMemo(() => `Hello ${name}`, [name]);
const summary = useMemo(() => `${greeting}, doubled: ${doubled}`, [greeting, doubled]);
// ❌ React 기본 패턴: 컴포넌트 단위 업데이트 (비효율)
const Dashboard = ({ user, posts, sidebar, notifications }) => {
// sidebar.collapsed만 바뀌어도 전체 컴포넌트 리렌더링
return (
<div>
<UserProfile user={user} /> {/* 불필요한 계산 */}
<PostList posts={posts} /> {/* 불필요한 계산 */}
<Sidebar collapsed={sidebar.collapsed} /> {/* 실제 변경 부분 */}
<NotificationPanel notifications={notifications} /> {/* 불필요한 계산 */}
</div>
);
};
// ✅ Svelte 정밀도를 React에 적용: 비트마스크처럼 정확한 분리
const Dashboard = () => {
return (
<div>
<UserProfileContainer /> {/* user 변경시에만 리렌더링 */}
<PostListContainer /> {/* posts 변경시에만 리렌더링 */}
<SidebarContainer /> {/* sidebar 변경시에만 리렌더링 */}
<NotificationContainer /> {/* notifications 변경시에만 리렌더링 */}
</div>
);
};
// 각 컨테이너는 해당 상태만 구독
const SidebarContainer = () => {
const collapsed = useSelector(state => state.sidebar.collapsed); // 정밀한 구독
return <Sidebar collapsed={collapsed} />;
};
// ❌ React 안티패턴: 모든 레이어가 컴포넌트에 뒤엉킴
const UserComponent = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
// 🔴 비즈니스 로직이 UI와 섞임
const addUser = async (userData) => {
setLoading(true); // UI 로직
const errors = validateUser(userData); // 비즈니스 로직
if (errors) { setErrors(errors); return; } // UI 로직
try {
const response = await api.createUser(userData); // 비즈니스 로직
setUsers(prev => [...prev, response.data]); // 상태 로직
setLoading(false); // UI 로직
} catch (error) {
setError(error.message); // UI 로직
}
};
return <div>{/* UI */}</div>;
};
// ✅ Svelte 컴파일러 패턴을 React에 적용
// 1단계: 순수 비즈니스 로직 (UserService.ts)
class UserService {
static validateUser(user) {
return user.email ? null : 'Email required';
}
static async createUser(userData) {
return await api.createUser(userData);
}
}
// 2단계: 상태 관리 (useUserStore.ts)
const useUserStore = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const addUser = useCallback(async (userData) => {
setLoading(true);
try {
const user = await UserService.createUser(userData);
setUsers(prev => [...prev, user]);
} finally {
setLoading(false);
}
}, []);
return { users, loading, addUser };
};
// 3단계: 순수 UI (UserComponent.jsx)
const UserComponent = () => {
const { users, loading, addUser } = useUserStore();
return (
<div>
{loading && <Spinner />}
<UserList users={users} />
<AddUserForm onSubmit={addUser} />
</div>
);
};
// 🔴 모든 파생 상태가 컴포넌트 내부에 얽혀있음
const BadReactPattern = () => {
const a = useSelector(state => state.a);
const b = useSelector(state => state.b);
// 🔴 매 렌더링마다 의존성 체크 + 계산 로직이 컴포넌트에 노출
const c = useMemo(() => a + b, [a, b]);
const d = useMemo(() => c * 2, [c]);
const e = useMemo(() => a * d, [a, d]);
return <div>{c}, {d}, {e}</div>;
};
// ✅ 복잡한 파생 상태 계산 로직을 순수 함수로 분리 (e.g., utils/calculateMetrics.ts)
export function calculateMetrics(a: number, b: number) {
const c = a + b;
const d = c * 2;
const e = a * d;
return { c, d, e };
}
// ✅ 컴포넌트에서는 이 함수를 단일 useMemo로 호출
import { calculateMetrics } from './utils/calculateMetrics';
const OptimizedWithPureFunction = () => {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// 🟢 복잡한 계산 과정은 캡슐화되고, 최상위 의존성 [a, b]만 신경 쓰면 됨
const metrics = useMemo(() => calculateMetrics(a, b), [a, b]);
return <div>{metrics.c}, {metrics.d}, {metrics.e}</div>;
};
쉽게 Svelte를 도입하기는 어렵지만,
Svelte의 철학을 React에 적용하는 것만으로도 상당한 개발자 경험 개선을 얻을 수 있을 것 같습니다.
가끔은 다른 프레임워크도 조사하면서 환기 하는 것도 좋은 것 같습니다.