immer.js는 불변성을 유지하면서 가변적으로 보이는 코드 작성을 돕는 라이브러리로 알고 있는데, immer는 always라는 의미가 있다고 한다. 이 의미와 immer.js의 사용에 관련성이 있는지 클로드에게 물어보았다.
☀️ immer라는 단어의 어원과 immer.js의 핵심 개념 사이에는 재미있는 연관성이 있습니다. immer는 독일어로 "항상, 언제나"(always)라는 의미를 가지고 있는데, 이는 다음을 보장하는 immer.js의 핵심 철학과 잘 맞아떨어집니다.
예시
🖥️ javascript
import produce from "immer"
const baseState = [
{ title: "Learn TypeScript", done: false },
{ title: "Try Immer", done: false }
]
const nextState = produce(baseState, draft => {
draft[1].done = true // 마치 직접 수정하는 것처럼 작성
})
console.log(baseState[1].done) // false (원본은 항상 불변)
console.log(nextState[1].done) // true (새로운 상태)
기술면접 스터디 중에 머릿속에서 여러 내용이 섞여 정리해본다.
상태 관리에서 불변성을 다루기 위한 도구, 주로 상태 업데이트 로직을 작성할 때 사용.
코드 분할(code splitting)과 비동기 컴포넌트 로딩을 위한 도구로
주로 성능 최적화와 로딩 상태 처리에 사용한다.
하지만 같은 React 애플리케이션 내에서 아래 예시처럼 사용 가능함.
🖥️ javascript
import { lazy, Suspense } from 'react';
import produce from 'immer';
import { useState } from 'react';
// React.lazy로 컴포넌트 지연 로딩
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
const [state, setState] = useState({ data: [] });
const updateState = () => {
// immer.js로 상태 업데이트
setState(produce(draft => {
draft.data.push('new item');
}));
};
return (
<div>
<button onClick={updateState}>Update State</button>
{/* Suspense로 지연 로딩 컴포넌트 감싸기 */}
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent data={state.data} />
</Suspense>
</div>
);
}
➡️ Suspense는 로딩이 지연될 때 뭘 보여줄지(fallback) 선언적으로 결정,
그 안에 들어가는 늦게 컴포넌트를 늦게 로딩하기 위해 React.lazy 사용,
이 과정 중에 불변성을 유지하는 상태 관리를 돕기 위한 immer.js!
"use strict" 구문을 사용하여 활성화.
JavaScript 코드를 더 엄격하게 실행하여 잠재적인 에러를 방지.
모든 JavaScript 환경(브라우저, Node.js 등)에서 사용 가능.
<React.StrictMode> 컴포넌트로 사용.
React 애플리케이션의 잠재적인 문제를 찾아내는 도구.
개발 모드에서만 동작하는 추가적인 검사와 경고를 활성화.
<React.StrictMode>를 친 기억이 없는데 혹시 next.js에서 자동으로 제공돼?Next.js 13 버전부터는 app 디렉토리를 사용하는 프로젝트에서 자동으로 Strict Mode가 활성화되어 있다. 비활성화하고 싶다면 next.config.js 파일에서 설정을 변경하면 된다.
🖥️ javascript
// next.config.js
module.exports = {
reactStrictMode: false,
}
(비활성화는 권장되지 않는다.)
프로젝트에서 폼의 인풋을 비제어 컴포넌트로 다룰 때 ref만 사용해 보았고 혹시 다른 방식이 있는지 궁금해졌다.
1. HTML form 태그의 name 속성과 onSubmit 이벤트를 활용
🖥️ javascript
function UncontrolledForm() {
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const name = formData.get('username');
const email = formData.get('email');
console.log({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" defaultValue="" />
<input type="email" name="email" defaultValue="" />
<button type="submit">제출</button>
</form>
);
}
2. defaultValue 또는 defaultChecked 속성 사용
🖥️ javascript
function UncontrolledComponent() {
return (
<div>
<input type="text" defaultValue="초기값" />
<input type="checkbox" defaultChecked={true} />
<textarea defaultValue="기본 텍스트" />
</div>
);
}
3. DOM API를 직접 사용하는 방법
🖥️ javascript
function UncontrolledWithDOM() {
const handleSubmit = (event) => {
event.preventDefault();
const form = event.target;
const username = form.elements.username.value;
const email = form.elements.email.value;
console.log({ username, email });
};
return (
<form onSubmit={handleSubmit}>
<input type="text" id="username" />
<input type="email" id="email" />
<button type="submit">제출</button>
</form>
);
}
➡️ defaultValue와 defaultChecked라는 속성이 눈에 띄긴 하지만,
코드만 보았을 때는 onChange event를 실시간으로 감지하지 않은 채 제출을 구현하면 즉, 제어 컴포넌트가 아닌 방식이면 비제어에 속한다고 판단하는 것으로보인다.
하지만 React에서는 일반적으로 제어 컴포넌트를 사용하는 것을 권장.
비제어 컴포넌트 사용을 고려해 볼 만한 경우는 다음과 같다.
(<input type="file">)flux 아키텍처 패턴을 공부하다가, 이와 다르게 단방향이 아닌 다른 아키텍처 패턴들의 종류와 그 데이터 흐름에 대한 질문이 나왔다.
MVC (Model-View-Controller) 패턴
🖥️ javascript
// Model
class UserModel {
constructor() {
this.name = '';
this.email = '';
}
setData(name, email) {
this.name = name;
this.email = email;
// 뷰에 변경 알림
this.notifyView();
}
}
// View
class UserView {
constructor(controller) {
this.controller = controller;
}
handleSubmit(name, email) {
// 컨트롤러에 데이터 전달
this.controller.updateUser(name, email);
}
render(data) {
console.log('Rendered:', data);
}
}
// Controller
class UserController {
constructor(model, view) {
this.model = model;
this.view = view;
}
updateUser(name, email) {
this.model.setData(name, email);
}
}
MVVM (Model-View-ViewModel) 패턴
(MVVM 본 적이 있다는 사실까지만 기억난다)
🖥️ javascript
// Model
class UserModel {
constructor() {
this.name = '';
this.email = '';
}
}
// ViewModel
class UserViewModel {
constructor(model) {
this.model = model;
this.observables = {
name: new Observable(''),
email: new Observable('')
};
}
updateData(name, email) {
this.model.name = name;
this.model.email = email;
this.observables.name.setValue(name);
this.observables.email.setValue(email);
}
}
// Observable for data binding
class Observable {
constructor(value) {
this.value = value;
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
}
setValue(newValue) {
this.value = newValue;
this.notify();
}
notify() {
this.subscribers.forEach(callback => callback(this.value));
}
}
Event-Driven Architecture
🖥️ javascript
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
// 사용 예시
const eventBus = new EventEmitter();
class UserService {
constructor() {
eventBus.on('userUpdated', this.handleUserUpdate);
}
handleUserUpdate(userData) {
console.log('User updated:', userData);
}
}
class ProfileComponent {
updateUser(data) {
// 이벤트 발생
eventBus.emit('userUpdated', data);
}
}
Hexagonal Architecture (Ports and Adapters)
🖥️ javascript
// Port (인터페이스)
interface UserRepository {
save(user: User): Promise<void>;
findById(id: string): Promise<User>;
}
// Primary Adapter (들어오는 요청 처리)
class UserController {
constructor(private userService: UserService) {}
async handleUserCreation(userData: UserData) {
await this.userService.createUser(userData);
}
}
// Secondary Adapter (외부 시스템과 통신)
class MongoUserRepository implements UserRepository {
async save(user: User): Promise<void> {
// MongoDB와 통신
}
async findById(id: string): Promise<User> {
// MongoDB에서 조회
}
}
각 아키텍처의 장단점을 이해하고, 프로젝트의 요구사항과 규모에 따라 적절한 패턴을 선택하도록 한다. 최근에는 패턴들을 혼합해서 사용하는 하이브리드 접근방식도 많이 사용.
툭 하면 듣는 비즈니스 로직이란 단어의 의미를 명확히 하고 넘어가고 싶어졌다.
정의 : 실제 비즈니스 규칙과 절차를 코드로 구현한 부분
왜 비즈니스로직이라고 불러?
서비스의 비즈니스에 기본적으로 들어가야 하는 업무적 코드를 다뤄서 그래?
☀️ 네, 정확히 이해하셨습니다! '비즈니스 로직'이라는 용어는 해당 서비스나 기업의 실제 업무 규칙과 절차를 코드로 구현했기 때문에 붙은 이름입니다.
이는 모두 각 비즈니스의 고유한 업무 규칙들이다. UI나 데이터베이스 같은 기술적인 부분과 달리, 이 로직들은 해당 비즈니스의 실제 운영 방식과 정책을 반영하기 때문에 '비즈니스 로직'이라고 부릅니다.
즉, 비즈니스 로직은 "이 서비스가 어떻게 동작해야 하는가"에 대한
실제 업무 규칙을 코드로 표현한 것이다.
1. 데이터 처리 규칙
🖥️ javascript
// 비즈니스 로직 예시: 주문 금액 계산
class OrderService {
calculateTotalPrice(order) {
let total = 0;
// 상품 가격 합계
order.items.forEach(item => {
total += item.price * item.quantity;
});
// 할인 적용
if (total > 50000) {
total *= 0.9; // 5만원 이상 구매시 10% 할인
}
// 배송비 계산
if (total < 30000) {
total += 3000; // 3만원 미만 구매시 배송비 3000원
}
return total;
}
}
2. 유효성 검증 규칙
🖥️ javascript
class UserService {
validateUser(user) {
// 사용자 나이 제한 확인
if (user.age < 19) {
throw new Error('성인만 가입 가능합니다');
}
// 비밀번호 규칙 검증
if (user.password.length < 8) {
throw new Error('비밀번호는 8자 이상이어야 합니다');
}
// 이메일 중복 확인
if (this.isEmailExists(user.email)) {
throw new Error('이미 사용중인 이메일입니다');
}
}
}
예를 들어 쇼핑몰의 경우
보통 비즈니스 로직은 프론트엔드보다 백엔드에 더 많이 구현되지만,
프론트엔드에서도 일부 비즈니스 로직이 필요할 수 있다.
번들(Bundle)은 여러 개의 파일과 모듈을 하나로 묶은 파일을 가리킨다. 웹 개발에서 주로 사용되는 용어로 크게 두 가지 관점에서 설명한다.
1. 성능 최적화 관점
번들링 전
🖥️ javascript
// 원래 파일들
// header.js
export const Header = () => { ... }
// footer.js
export const Footer = () => { ... }
// main.js
import { Header } from './header'
import { Footer } from './footer'
번들링 후
🖥️ javascript
// bundle.js (하나의 파일로 합쳐짐)
const Header = () => { ... }
const Footer = () => { ... }
// 모든 코드가 하나의 파일에 최적화되어 포함
2. 의존성 관리 관점
🖥️ javascript
// package.json
{
"dependencies": {
"react": "^18.0.0",
"lodash": "^4.17.21"
}
}
이런 외부 라이브러리들도 최종적으로는 애플리케이션 코드와 함께 번들로 묶인다.
HTTP 요청 횟수 감소 (여러 파일을 따로 받지 않아도 됨)
코드 최적화 (불필요한 공백 제거, 코드 압축)
브라우저 호환성 보장 (최신 문법을 구버전 브라우저에서도 동작하도록 변환)
모듈 의존성 관리 용이
대표적인 번들러
❓빌드 도구는?
스트리밍은 데이터를 작은 단위로 연속적으로 전송하고 받는 즉시 처리하는 방식이다. React에서 Suspense와 함께 사용될 때는 서버에서 렌더링된 HTML을 점진적으로 전송하는 것을 의미한다.
전통적인 방식
🖥️ javascript
// 모든 데이터를 기다린 후 한번에 렌더링
async function Page() {
const data = await fetchAllData() // 오래 걸리는 작업
return <MainComponent data={data} />
}
스트리밍 방식
🖥️ javascript
function Page() {
return (
<>
<Header /> {/* 즉시 렌더링 */}
<Suspense fallback={<Loading />}>
<SlowComponent /> {/* 데이터를 기다리는 동안 Loading 표시 */}
</Suspense>
<Footer /> {/* 즉시 렌더링 */}
</>
)
}