💤 Lazy Loading
현대 소프트웨어 개발에서 **최적화(Optimization)**는 성능 향상과 사용자 경험(UX) 개선의 핵심 요소입니다. 특히 초기 로딩 시간 감소와 불필요한 자원 낭비 방지를 위해 필수적으로 활용되는 기법 중 하나가 바로 **Lazy Loading (지연 로딩)**입니다. 이 글에서는 Lazy Loading의 단순한 정의를 넘어, 그 내부 작동 원리, 다양한 아키텍처 패턴, 실전 적용 사례, 그리고 컴퓨터 과학적 함의까지 깊이 있게 파헤쳐 보겠습니다.
1. Lazy Loading이란 무엇인가? (개념 및 동기)
Lazy Loading은 말 그대로 "게으른 로딩"입니다. 즉, 애플리케이션 초기 구동 시점에 모든 리소스를 한꺼번에 로드하는 Eager Loading (즉시 로딩) 방식과는 달리, **필요한 시점(Just-in-Time)**까지 데이터나 컴포넌트의 로딩을 지연시키는 최적화 전략입니다.
📚 정식 정의: Lazy Loading은 애플리케이션의 초기 실행 시점이 아닌, 특정 데이터나 컴포넌트가 실제로 사용되거나 사용자에게 노출되는 시점에 해당 리소스를 동적으로(Dynamically) 로드하는 최적화 전략이자 디자인 패턴입니다.
왜 Lazy Loading이 필요한가? (문제점 및 해결)
현대의 웹 및 애플리케이션은 점점 더 복잡해지고, 대량의 미디어 파일, 다양한 모듈, 방대한 데이터 셋을 포함합니다. 이 모든 것을 초기 로딩 시점에 한꺼번에 불러온다면 다음과 같은 문제가 발생합니다.
- 초기 로딩 시간 증가 (Time-to-Interactive, TTI 지연): 거대한 번들 사이즈는 네트워크 전송 시간과 파싱(Parsing), 컴파일(Compiling), 렌더링(Rendering) 시간을 기하급수적으로 늘려 사용자가 애플리케이션과 상호작용할 수 있는 시점(TTI)을 지연시킵니다.
- 메모리 점유율 증가 및 낭비 (Memory Footprint): 당장 필요하지 않은 수많은 객체, 이미지, 모듈이 메모리에 상주하게 되어 시스템의 메모리 효율성을 저하시키고, 제한된 자원에서 다른 중요한 작업의 성능을 저해할 수 있습니다. 이는 **메모리 누수(Memory Leak)**의 잠재적 위험을 높입니다.
- 사용자 경험(UX) 저해: 느린 초기 로딩은 사용자의 이탈률을 높이고, 애플리케이션이 반응성이 떨어진다는 인상을 줍니다. 즉, 초기 로딩 시점에 모든 것을 보여줄 필요가 없는 경우, 불필요한 지연은 사용자 만족도를 떨어뜨립니다.
Lazy Loading은 이러한 문제들을 해결하여 초기 로딩 시간 감소, 메모리 효율 향상, 그리고 사용자 경험(UX) 개선이라는 세 마리 토끼를 잡습니다. 이는 리소스의 효율적 관리와 온디맨드(On-Demand) 제공이라는 측면에서 매우 중요한 기법입니다.
2. Lazy Loading의 구조적 원리 (디자인 패턴 및 메커니즘)
Lazy Loading은 다양한 방식으로 구현될 수 있지만, 그 근본적인 구조적 원리는 특정 디자인 패턴과 메커니즘에 기반을 둡니다.
2.1. 프록시 패턴 (Proxy Pattern) 또는 래퍼(Wrapper) 객체 사용
가장 고전적이고 일반적인 Lazy Loading의 구현 원리 중 하나는 프록시(Proxy) 패턴을 활용하는 것입니다. 이는 Gang of Four (GoF) 디자인 패턴 중 하나로, 실제 객체에 대한 접근을 제어하고, 필요할 때 실제 객체를 생성하여 지연 로딩을 구현합니다.
- 개념: 실제 객체(RealSubject) 대신 **프록시 객체(Proxy)**를 먼저 생성하고 클라이언트에게 노출합니다. 클라이언트가 프록시 객체의 특정 메서드를 호출할 때 비로소 실제 객체를 초기화(Lazy Initialization)하고 해당 메서드를 실제 객체에 위임하여 실행합니다.
- 구조적 이점: 실제 객체의 생성이 무겁거나 네트워크/DB 접근과 같은 시간이 많이 소요되는 작업일 때 유용합니다. 클라이언트는 프록시를 통해 실제 객체가 즉시 준비된 것처럼 상호작용할 수 있어, 시스템의 응답성을 높입니다.
// Java 예시 (Proxy Pattern 기반)
interface Image {
void display();
}
// 실제 객체: 이미지를 실제로 로드하고 표시하는 무거운 작업
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
// 실제 이미지 로딩 (시간 소요 작업)
System.out.println("Loading image: " + filename);
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}
// 프록시 객체: 실제 이미지 로딩을 지연시킴
class ImageProxy implements Image {
private RealImage realImage; // 실제 객체에 대한 참조
private String filename;
public ImageProxy(String filename) {
this.filename = filename;
// 프록시 생성 시에는 실제 이미지를 로드하지 않음
System.out.println("Creating proxy for: " + filename);
}
@Override
public void display() {
if (realImage == null) {
// 실제 이미지가 필요한 순간 (display() 호출 시점)에 로드
System.out.println("Real image required. Performing lazy load...");
realImage = new RealImage(filename); // 여기서 Lazy Load 발생!
}
realImage.display(); // 실제 객체에 작업 위임
}
}
// 사용 예시
public class LazyLoadExample {
public static void main(String[] args) {
Image image1 = new ImageProxy("large_photo_1.jpg"); // 프록시 생성, 실제 로딩 없음
Image image2 = new ImageProxy("large_photo_2.jpg"); // 프록시 생성, 실제 로딩 없음
System.out.println("\n--- Initial setup complete ---\n");
System.out.println("Attempting to display image1:");
image1.display(); // image1이 처음으로 필요할 때 로드됨
System.out.println("\nAttempting to display image2:");
image2.display(); // image2가 처음으로 필요할 때 로드됨
}
}
출력:
Creating proxy for: large_photo_1.jpg
Creating proxy for: large_photo_2.jpg
--- Initial setup complete ---
Attempting to display image1:
Real image required. Performing lazy load...
Loading image: large_photo_1.jpg
Displaying image: large_photo_1.jpg
Attempting to display image2:
Real image required. Performing lazy load...
Loading image: large_photo_2.jpg
Displaying image: large_photo_2.jpg
위 예시에서 ImageProxy 객체가 RealImage 객체의 생성을 display() 메서드가 처음 호출될 때까지 지연시키는 것을 볼 수 있습니다. 이는 시스템의 초기화 부담을 줄여줍니다.
2.2. 조건부 로딩 (Conditional Loading) 및 옵저버 패턴
웹 환경, 특히 프론트엔드에서는 사용자의 뷰포트(Viewport) 진입 여부나 특정 이벤트 발생을 조건으로 리소스를 로드하는 방식이 널리 사용됩니다. 이는 일종의 **이벤트-기반 지연 로딩(Event-Driven Lazy Loading)**으로 볼 수 있습니다.
- 메커니즘:
- 이벤트 리스너/옵저버 등록: 특정 DOM 요소(예: 이미지)에 대한 이벤트 리스너(스크롤 이벤트 등)를 등록하거나, Intersection Observer API와 같은 비동기적인 메커니즘을 사용하여 해당 요소의 가시성을 감지합니다.
- 조건 충족 시 로딩: 요소가 뷰포트에 진입했거나, 사용자가 특정 버튼을 클릭하는 등 사전에 정의된 조건이 충족되면, 해당 리소스의 실제 로딩(예: 이미지 src 속성 할당, 데이터 페칭)을 트리거합니다.
// JavaScript 예시 (Intersection Observer API 활용)
// Intersection Observer는 비동기적으로 요소의 가시성 변화를 감지하여
// 메인 스레드에 부담을 주지 않고 효율적인 감시를 가능하게 합니다.
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) { // 요소가 뷰포트에 진입했는지 확인
const img = entry.target;
if (img.dataset.src) { // data-src 속성이 있는 경우에만 처리 (실제 src는 아직 로드되지 않음)
img.src = img.dataset.src; // Lazy Load: 실제 이미지 경로를 src에 할당
img.removeAttribute('data-src'); // 중복 로딩 방지를 위해 data-src 제거
}
observer.unobserve(img); // 한 번 로드된 이미지는 더 이상 관찰하지 않음
}
});
}, {
rootMargin: '0px 0px 50px 0px', // 뷰포트 하단 50px 전에 미리 로드 시작
threshold: 0.01 // 요소의 1%만 보여도 콜백 실행
});
// 모든 Lazy Load 대상 이미지에 옵저버 연결
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
이 방식은 특히 이미지 갤러리, 무한 스크롤(Infinite Scroll) 리스트 등 대량의 미디어를 포함하는 웹 페이지에서 초기 렌더링 성능을 획기적으로 개선합니다.
3. Lazy vs Eager Loading: 설계 철학의 비교
Lazy Loading과 Eager Loading은 리소스 로딩 시점에 대한 설계 철학의 차이를 보여줍니다. 둘 중 어느 하나가 절대적으로 우수하다고 할 수 없으며, 애플리케이션의 특성과 요구사항에 따라 적절히 조합하여 사용하는 것이 중요합니다.
| 로딩 시점 | 실제 필요 시, 또는 특정 조건(뷰포트 진입 등) 충족 시 | 애플리케이션 초기 구동 시 |
| 초기 성능 | 빠름: 불필요한 리소스 로드 생략, 초기 로딩 시간 단축 | 느림: 모든 리소스 로드, 초기 로딩 시간 증가 |
| 메모리 사용 | 낮음: 필요한 리소스만 메모리에 유지, 효율적 자원 관리 | 높음: 사용 여부와 무관하게 모든 리소스 메모리 점유 |
| 리소스 전송량 | 초기 전송량 낮음: 네트워크 대역폭 효율적 사용 | 초기 전송량 높음: 대역폭 소모 증가 |
| 구현 복잡도 | 높음: 프록시 패턴, 조건부 로딩 로직, 에러 핸들링 등 추가 로직 필요 | 낮음: 대부분의 경우 기본 동작 |
| 종속성 관리 | 복잡도 있음: 동적으로 로드되는 모듈/데이터의 종속성 해결 필요 | 단순: 모든 종속성이 초기부터 명확 |
| 사용자 경험 | 초기 진입 빠름, 이후 약간의 지연 가능성. 반응성(Responsiveness) 중시 | 초기 진입 느림, 이후 부드러운 전환. 예측 가능성(Predictability) 중시 |
| 주요 사용 예시 | 이미지, 비디오, SPA의 라우트/컴포넌트, DB 관계형 데이터, 모듈 번들 | 핵심 구성 파일, 초기 렌더링에 필수적인 CSS/JS, 공통 유틸리티 모듈 |
| 컴퓨터 과학 원리 | 지연 평가(Lazy Evaluation), 온디맨드 로딩(On-Demand Loading) | 즉시 평가(Eager Evaluation) |
4. 다양한 기술 스택에서의 Lazy Loading 구현
Lazy Loading은 특정 프레임워크나 언어에 국한된 개념이 아니라, 소프트웨어 아키텍처 전반에 걸쳐 적용될 수 있는 보편적인 최적화 전략입니다.
4.1. 웹 프론트엔드 (React, Vue, Angular 등)
모던 웹 프론트엔드 프레임워크는 **코드 스플리팅(Code Splitting)**을 통해 컴포넌트나 라우트(Route) 단위의 Lazy Loading을 강력하게 지원합니다. 이는 Webpack, Rollup, Vite 등 번들러의 기능과 결합하여 구현됩니다.
// React에서 Lazy Loading Component (React.lazy와 Suspense 활용)
// Webpack의 dynamic import() 기능과 결합하여 코드 스플리팅을 구현합니다.
import React, { Suspense, lazy } from 'react';
// MyComponent는 이 시점에 로드되지 않고, 처음 렌더링될 때 비로소 로드됩니다.
const MyComponent = lazy(() => import('./MyComponent')); // 동적 임포트
function App() {
return (
// Suspense는 lazy 로드되는 컴포넌트가 로드될 때까지 대체 UI를 보여줍니다.
<Suspense fallback={<div>Loading MyComponent...</div>}>
<MyComponent /> {/* MyComponent가 렌더링되는 시점에 로드 시작 */}
</Suspense>
);
}
// React Router와 결합하여 라우트 기반 코드 스플리팅
// import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// const HomePage = lazy(() => import('./pages/HomePage'));
// const AboutPage = lazy(() => import('./pages/AboutPage'));
//
// function AppRouter() {
// return (
// <Router>
// <Suspense fallback={<div>페이지 로딩 중...</div>}>
// <Routes>
// <Route path="/" element={<HomePage />} />
// <Route path="/about" element={<AboutPage />} />
// </Routes>
// </Suspense>
// </Router>
// );
// }
이 방식은 특히 SPA(Single Page Application)에서 초기 번들 사이즈를 줄이고, 사용자가 특정 라우트로 이동할 때만 해당 페이지의 코드를 로드하여 First Contentful Paint (FCP) 및 **Time to Interactive (TTI)**를 개선하는 데 필수적입니다.
4.2. Database ORM (Object-Relational Mapping, e.g., Hibernate in Java, SQLAlchemy in Python)
데이터베이스 ORM 프레임워크에서는 연관 관계(Association) 로딩 시 Lazy Loading을 기본적으로 지원합니다. 이는 N+1 쿼리 문제와 같은 성능 이슈를 해결하는 데 중요합니다.
- 개념: 엔티티 간의 연관 관계(예: User와 Post의 OneToMany)를 미리 로드하지 않고, 해당 연관 컬렉션(예: user.getPosts())에 접근하는 시점에 비로소 데이터베이스에서 데이터를 조회하여 로드합니다.
- 구현: 대부분의 ORM은 어노테이션(Java의 @OneToMany(fetch = FetchType.LAZY))이나 설정(Python SQLAlchemy의 lazy='dynamic')을 통해 이를 제어합니다.
// Hibernate (Java ORM) 예시
import jakarta.persistence.*;
import java.util.List;
@Entity // JPA 엔티티 선언
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// FetchType.LAZY 설정으로 posts 컬렉션은 User 엔티티 로드 시 함께 로드되지 않음
// posts 컬렉션에 접근(예: user.getPosts())하는 순간에 DB에서 조회하여 로드됨
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL)
private List<Post> posts;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<Post> getPosts() { return posts; } // 이 메서드 호출 시 Lazy Load 발생
public void setPosts(List<Post> posts) { this.posts = posts; }
}
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY) // Post에서 User로의 ManyToOne 관계도 Lazy로 설정 가능
@JoinColumn(name = "user_id")
private User user;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}
// Lazy Loading 동작 예시
// public static void main(String[] args) {
// EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
// EntityManager em = emf.createEntityManager();
//
// em.getTransaction().begin();
// User user = new User();
// user.setName("Alice");
// em.persist(user);
// em.getTransaction().commit();
//
// em.clear(); // 영속성 컨텍스트 비우기
//
// User retrievedUser = em.find(User.class, user.getId()); // User만 로드, Post는 로드 안 됨
// System.out.println("User retrieved: " + retrievedUser.getName());
//
// // 이 시점에 posts 컬렉션에 접근하면 Lazy Loading 발생
// System.out.println("Posts size: " + retrievedUser.getPosts().size()); // 여기서 DB 쿼리 발생!
//
// em.close();
// emf.close();
// }
User 엔티티를 로드할 때 posts 컬렉션은 즉시 로드되지 않습니다. getPosts() 메서드가 호출되는 시점에 별도의 DB 쿼리가 발생하여 관련된 Post들을 로드하게 됩니다. 이는 필요한 데이터만 메모리에 올려 DB 트랜잭션과 메모리 사용량을 최적화합니다.
4.3. 모바일 앱 (Android, iOS)
모바일 앱 환경에서도 Lazy Loading은 리소스 관리의 핵심입니다.
- 이미지 라이브러리: Glide, Picasso (Android), Kingfisher (iOS)와 같은 이미지 로딩 라이브러리들은 내부적으로 Lazy Loading 전략을 사용하여, 이미지가 뷰포트에 나타날 때만 로드하고 캐싱합니다.
- RecyclerView / UITableView: 리스트 뷰에서 화면에 보이는 아이템만 렌더링하고, 스크롤에 따라 새로 보이는 아이템의 이미지를 지연 로드합니다. 이는 가상화(Virtualization) 또는 윈도잉(Windowing) 기법과 결합되어 수천 개의 아이템도 부드럽게 스크롤할 수 있게 합니다.
5. Lazy Loading 도입 시 고려사항 및 도전 과제
Lazy Loading은 강력한 최적화 기법이지만, 잘못 적용하면 오히려 사용자 경험을 해치거나 예상치 못한 문제를 야기할 수 있습니다.
- 사용자 행동 예측의 중요성: 모든 리소스에 Lazy Loading을 적용하는 것이 능사는 아닙니다. 사용자가 거의 확실하게 접근할 데이터나 초기 화면에 필수적인 리소스는 Eager Loading으로 미리 로드하는 것이 UX에 더 좋습니다. 사용 시점 예측이 불가능한 데이터에 딜레이가 발생하면 오히려 '버벅임'으로 느껴질 수 있습니다.
- 에러 핸들링 및 폴백(Fallback) UI: 동적으로 로드되는 리소스는 네트워크 오류, 서버 응답 지연 등으로 인해 로딩에 실패할 수 있습니다. 이때 사용자에게 적절한 에러 메시지나 **폴백 UI (예: 스켈레톤 UI, 로딩 스피너)**를 제공하여 공백 화면이나 깨진 콘텐츠를 방지하는 것이 중요합니다.
- SEO 이슈 (웹 프론트엔드): 클라이언트 측에서 JavaScript를 통해 Lazy Loading되는 콘텐츠는 검색 엔진 크롤러가 초기 로드 시점에는 접근하지 못할 수 있습니다. 이는 **검색 엔진 최적화(SEO)**에 불리하게 작용할 수 있습니다. 서버 사이드 렌더링(SSR) 또는 **정적 사이트 생성(SSG)**과 Lazy Loading을 조합하여 보완하는 전략이 필요합니다.
- 초기 렌더링 시점 판단 기준 명확화: Intersection Observer의 rootMargin이나 threshold와 같은 옵션을 조절하여 너무 늦게 로드되지 않도록 적절한 임계값을 설정해야 합니다. '지연'이 '버벅임'으로 느껴지지 않도록 사용자 기대치를 충족시키는 것이 중요합니다.
- 깜빡임 (Flickering) 현상: 이미지가 Lazy Loading될 때, 이미지의 공간을 미리 확보하지 않으면 로딩 완료 후 갑자기 레이아웃이 변경되는 깜빡임(Flickering) 현상이 발생하여 UX를 해칠 수 있습니다. width와 height를 미리 지정하거나 aspect-ratio CSS 속성을 활용하여 공간을 예약하는 것이 좋습니다.
6. Lazy Loading의 최적 활용 전략 (고급 기법)
Lazy Loading의 효과를 극대화하고 위에서 언급된 문제점들을 해결하기 위한 다양한 전략들이 존재합니다.
- 6.1. 코드 스플리팅 (Code Splitting):
- 설명: Webpack, Vite, Rollup 등 모던 번들러의 핵심 기능으로, 애플리케이션의 JavaScript, CSS 코드를 여러 개의 작은 번들 파일로 분리합니다. 특정 라우트나 컴포넌트가 필요할 때만 해당 번들을 로드하여 초기 로딩 시간을 획기적으로 줄입니다.
- 원리: 동적 임포트(Dynamic Import) (import()) 문법을 사용하면, 해당 모듈은 별도의 청크(Chunk)로 분리되어 필요할 때 비동기적으로 로드됩니다.
- 6.2. 이미지/비디오 Lazy Load 표준화:
- 설명: HTML 표준에는 <img loading="lazy"> 및 <iframe loading="lazy"> 속성이 추가되어 브라우저 자체적으로 Lazy Loading을 지원합니다. 이는 JavaScript 코드를 작성할 필요 없이 간단하게 적용할 수 있습니다.
- 작동 방식: 브라우저가 해당 요소가 뷰포트 근처에 올 때까지 이미지/프레임 로딩을 지연시킵니다.
- 6.3. SSR (Server-Side Rendering) + Lazy Loading 조합:
- 설명: Next.js, Nuxt.js 등 SSR 프레임워크를 사용하여 초기 페이지는 서버에서 렌더링하여 SEO 문제를 해결하고 빠른 FCP를 달성합니다. 이후 사용자 상호작용이 필요한 부분이나 스크롤 시점에 Lazy Loading을 적용하여 클라이언트 사이드 번들 크기를 줄이고 TTI를 개선합니다.
- 이점: SEO와 UX를 동시에 확보할 수 있는 강력한 전략입니다.
- 6.4. 가상 스크롤 (Virtualization / Windowing):
- 설명: 수천, 수만 개의 리스트 아이템을 가진 경우, 모든 DOM 요소를 한꺼번에 렌더링하는 대신, 사용자 뷰포트에 보이는 극히 일부의 아이템만 실제 DOM으로 렌더링하고 나머지는 가상으로 처리하는 기법입니다. react-window, react-virtualized와 같은 라이브러리가 이를 구현합니다.
- 이점: 엄청난 양의 DOM 요소로 인한 렌더링 성능 저하와 메모리 사용량 증가 문제를 해결하며, 보이는 부분의 데이터만 Lazy Loading하여 효율을 극대화합니다.
7. 결론: Lazy Loading, 단순한 기술을 넘어선 아키텍처 철학
Lazy Loading은 단순히 코드를 최적화하는 기술적 기법을 넘어섭니다. 이는 자원을 어떻게 효율적으로 관리할 것인가, 사용자 경험을 어떻게 설계할 것인가, 그리고 애플리케이션의 확장성과 유지보수성을 어떻게 확보할 것인가에 대한 깊은 아키텍처적 철학이 반영된 전략입니다.
컴퓨터 공학적 관점에서 Lazy Loading은 지연 평가(Lazy Evaluation) 원칙의 실제 적용 사례이며, **온디맨드 리소스 할당(On-Demand Resource Allocation)**을 통해 시스템의 전반적인 효율성을 극대화합니다. 초기 로딩 시 불필요한 비용을 회피하고, 리소스가 실제로 필요한 순간에만 로드하는 이 "게으른" 접근 방식은 현대 소프트웨어 개발에서 성능, 확장성, 사용자 경험 세 마리 토끼를 모두 잡는 필수적인 도구입니다.
❝ 지금 당장 필요하지 않은 것들은, 나중에 불러도 늦지 않다. ❞
이 원칙을 애플리케이션 설계의 다양한 레이어에 적용함으로써, 우리는 더 빠르고, 더 효율적이며, 궁극적으로 더 나은 사용자 경험을 제공하는 소프트웨어를 구축할 수 있을 것입니다.
'코드의 해부학' 카테고리의 다른 글
| TCO (Tail Call Optimization): 디지털 연금술 — 스택의 한계를 넘어서는 코드의 변성(變性) (0) | 2025.06.01 |
|---|---|
| 코드 분할(Code Splitting) : 디지털 미니멀리즘 (0) | 2025.06.01 |
| 이벤트 위임(Event Delegation) : '분산'을 '집중'으로 전환하는 지능형 패턴 (1) | 2025.06.01 |
| 디바운스(Debounce)와 스로틀(Throttle) : 이벤트 폭주 시대의 지휘자 (0) | 2025.06.01 |
| 메모이제이션(Memoization) : CPU를 위한 다이어트 (0) | 2025.06.01 |