immer.js - immer 의미

immer.js는 불변성을 유지하면서 가변적으로 보이는 코드 작성을 돕는 라이브러리로 알고 있는데, immer는 always라는 의미가 있다고 한다. 이 의미와 immer.js의 사용에 관련성이 있는지 클로드에게 물어보았다.

  • ☀️ immer라는 단어의 어원과 immer.js의 핵심 개념 사이에는 재미있는 연관성이 있습니다. immer는 독일어로 "항상, 언제나"(always)라는 의미를 가지고 있는데, 이는 다음을 보장하는 immer.js의 핵심 철학과 잘 맞아떨어집니다.

    • 불변성(immutability)이 항상 보장됩니다.
      상태를 업데이트할 때마다 원본 상태는 절대 변경되지 않고 새로운 상태가 만들어집니다.
    • 개발자가 항상 직접 객체를 수정하는 것처럼 친숙한 문법 사용을 보장합니다.

예시

🖥️ 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 (새로운 상태)
  • immer.js는 독일 라이브러리야?
  • ☀️ 아니요, Michel Weststrate라는 네덜란드 개발자가 만든 JavaScript 라이브러리입니다. Michel Weststrate는 MobX라는 유명한 상태 관리 라이브러리도 만든 개발자인데요. immer는 단순히 라이브러리의 핵심 개념을 잘 표현하는 단어였기 때문에 사용했습니다. 현재는 오픈소스로 관리되고 있으며, React나 Redux와 같은 프레임워크/라이브러리와 함께 전 세계적으로 널리 사용되고 있습니다. 재미있는 점은 네덜란드어에서도 "immer"라는 단어가 비슷한 의미("항상")로 사용된다는 것인데, 이는 독일어와 네덜란드어가 같은 게르만어족에 속하기 때문입니다.

immer.js - suspense, react.lazy와 사용?

기술면접 스터디 중에 머릿속에서 여러 내용이 섞여 정리해본다.

  • ☀️ immer.js는 React의 Suspense나 React.lazy와 직접적인 연관성은 없습니다. 이들은 서로 다른 문제를 해결하기 위한 도구들입니다.

immer.js

상태 관리에서 불변성을 다루기 위한 도구, 주로 상태 업데이트 로직을 작성할 때 사용.

Suspense와 React.lazy

코드 분할(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!


Strict Mode - JS, React

JavaScript의 Strict 모드

"use strict" 구문을 사용하여 활성화.
JavaScript 코드를 더 엄격하게 실행하여 잠재적인 에러를 방지.
모든 JavaScript 환경(브라우저, Node.js 등)에서 사용 가능.

  • 특징
    • 선언되지 않은 변수 사용 금지
    • this의 글로벌 객체 참조 제한
    • 중복된 파라미터 이름 금지
    • 8진수 리터럴 사용 제한

React의 StrictMode

<React.StrictMode> 컴포넌트로 사용.
React 애플리케이션의 잠재적인 문제를 찾아내는 도구.
개발 모드에서만 동작하는 추가적인 검사와 경고를 활성화.

  • 주요 기능
    • 안전하지 않은 생명주기 메서드 감지
    • 레거시 문자열 ref 사용 감지
    • 예상치 못한 부작용 감지

  • 내가 코딩 중에 <React.StrictMode>를 친 기억이 없는데 혹시 next.js에서 자동으로 제공돼?
  • ☀️ 네, 맞습니다! Next.js는 기본적으로 React의 Strict Mode를 활성화하고 있습니다.

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>
  );
}

➡️ defaultValuedefaultChecked라는 속성이 눈에 띄긴 하지만,
코드만 보았을 때는 onChange event를 실시간으로 감지하지 않은 채 제출을 구현하면 즉, 제어 컴포넌트가 아닌 방식이면 비제어에 속한다고 판단하는 것으로보인다.

하지만 React에서는 일반적으로 제어 컴포넌트를 사용하는 것을 권장.
비제어 컴포넌트 사용을 고려해 볼 만한 경우는 다음과 같다.

  • 파일 업로드 input(<input type="file">)
  • 레거시 코드나 비-React 라이브러리와의 통합
  • 매우 간단한 폼으로 실시간 유효성 검사가 필요없는 경우
  • 성능이 매우 중요한 경우 (많은 수의 폼 요소가 있는 경우)

아키텍처 패턴

flux 아키텍처 패턴을 공부하다가, 이와 다르게 단방향이 아닌 다른 아키텍처 패턴들의 종류와 그 데이터 흐름에 대한 질문이 나왔다.

MVC (Model-View-Controller) 패턴

  • 양방향 데이터 흐름
  • View → Controller → Model → View 순환 구조
  • 복잡한 애플리케이션에서는 의존성 추적이 어려울 수 있음
🖥️ 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 본 적이 있다는 사실까지만 기억난다)

  • 데이터 바인딩을 통한 자동 동기화
  • View ↔ ViewModel ↔ Model 양방향 바인딩
  • Angular가 대표적인 예시
🖥️ 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('이미 사용중인 이메일입니다');
    }
  }
}

UI 로직 vs 비즈니스 로직

  • UI 로직 : 화면 표시, 버튼 클릭, 애니메이션 등
  • 비즈니스 로직 : 실제 데이터 처리, 계산, 규칙 적용 등

예를 들어 쇼핑몰의 경우

  • UI 로직 : 장바구니 버튼 표시, 수량 입력 폼
  • 비즈니스 로직 : 할인율 계산, 재고 확인, 주문 가능 여부 판단

보통 비즈니스 로직은 프론트엔드보다 백엔드에 더 많이 구현되지만,
프론트엔드에서도 일부 비즈니스 로직이 필요할 수 있다.


번들

번들(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 요청 횟수 감소 (여러 파일을 따로 받지 않아도 됨)

  • 코드 최적화 (불필요한 공백 제거, 코드 압축)

  • 브라우저 호환성 보장 (최신 문법을 구버전 브라우저에서도 동작하도록 변환)

  • 모듈 의존성 관리 용이

  • 대표적인 번들러

    • Webpack
    • Rollup
    • Vite
    • Parcel

❓빌드 도구는?


스트리밍 방식

스트리밍은 데이터를 작은 단위로 연속적으로 전송하고 받는 즉시 처리하는 방식이다. React에서 Suspense와 함께 사용될 때는 서버에서 렌더링된 HTML을 점진적으로 전송하는 것을 의미한다.

전통적인 방식 vs 스트리밍 방식

전통적인 방식

🖥️ javascript

// 모든 데이터를 기다린 후 한번에 렌더링
async function Page() {
  const data = await fetchAllData() // 오래 걸리는 작업
  return <MainComponent data={data} />  
}

스트리밍 방식

🖥️ javascript


function Page() {
  return (
    <>
      <Header /> {/* 즉시 렌더링 */}
      <Suspense fallback={<Loading />}>
        <SlowComponent /> {/* 데이터를 기다리는 동안 Loading 표시 */}
      </Suspense>
      <Footer /> {/* 즉시 렌더링 */}
    </>
  )
}
  • 스트리밍의 장점
    • 초기 로딩 시간 개선 (ready-to-show)
    • 점진적인 페이지 로드
    • 더 나은 사용자 경험 제공
  • 스트리밍 방식이 유용한 경우
    • 데이터 fetch가 오래 걸리는 컴포넌트
    • 큰 이미지나 미디어 파일 로딩
    • 복잡한 계산이 필요한 컴포넌트
    • SEO가 중요한 서버 사이드 렌더링
profile
아무튼, 개발자

0개의 댓글