본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 27. · 0 Views
React와 Next.js 통합 완벽 가이드
React 컴포넌트 설계부터 Next.js의 SSR과 CSR까지, 현대 웹 개발의 핵심을 다룹니다. 실무에서 바로 적용할 수 있는 패턴과 함께 로딩 상태 관리, 에러 처리까지 체계적으로 배워봅니다.
목차
1. React 컴포넌트 설계
김개발 씨는 입사 첫 주에 선배로부터 작은 기능 하나를 맡았습니다. "이 버튼 컴포넌트 하나만 만들어 볼래요?" 간단해 보였는데, 막상 만들고 보니 선배가 고개를 갸우뚱합니다.
"음, 이걸 다른 곳에서도 쓰려면 어떻게 해야 할까요?"
React 컴포넌트 설계는 레고 블록을 조립하듯 UI를 작은 단위로 나누는 것입니다. 마치 요리사가 재료를 미리 손질해두면 어떤 요리든 빠르게 만들 수 있는 것처럼, 잘 설계된 컴포넌트는 어디서든 재사용할 수 있습니다.
이것을 제대로 이해하면 유지보수가 쉽고 확장 가능한 코드를 작성할 수 있습니다.
다음 코드를 살펴봅시다.
// 재사용 가능한 Button 컴포넌트 설계
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
disabled?: boolean;
}
// 핵심 포인트: Props로 유연성을 확보합니다
export function Button({
children,
variant = 'primary',
size = 'md',
onClick,
disabled = false
}: ButtonProps) {
// 여기서 variant와 size에 따라 스타일이 결정됩니다
const baseStyle = 'rounded font-medium transition-colors';
const variantStyle = variant === 'primary'
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-200 text-gray-800 hover:bg-gray-300';
return (
<button
className={`${baseStyle} ${variantStyle}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 오늘도 열심히 코드를 작성하던 중, 이상한 상황에 마주쳤습니다.
분명히 버튼을 하나 만들었는데, 다른 페이지에서 비슷한 버튼이 또 필요하다고 합니다. 복사해서 붙여넣기를 해야 할까요?
선배 개발자 박시니어 씨가 다가와 코드를 살펴봅니다. "아, 여기가 문제네요.
컴포넌트 설계를 제대로 이해하지 못해서 생긴 상황이에요. 처음부터 재사용을 고려해서 만들어야 합니다." 그렇다면 컴포넌트 설계란 정확히 무엇일까요?
쉽게 비유하자면, 컴포넌트는 마치 레고 블록과 같습니다. 레고 블록 하나하나는 단순하지만, 조합하면 집도, 자동차도, 우주선도 만들 수 있습니다.
각 블록이 독립적이면서도 서로 연결될 수 있도록 설계되어 있기 때문입니다. React 컴포넌트도 마찬가지로, 하나의 작은 UI 단위가 독립적으로 동작하면서도 다른 컴포넌트와 자유롭게 조합될 수 있어야 합니다.
컴포넌트 설계 개념이 없던 시절에는 어땠을까요? 개발자들은 비슷한 UI가 필요할 때마다 코드를 복사해서 붙여넣었습니다.
버튼이 10개 필요하면 10번 복사했습니다. 문제는 그 버튼의 색상을 바꿔야 할 때 발생했습니다.
10군데를 모두 찾아서 수정해야 했고, 하나라도 빠뜨리면 일관성이 깨졌습니다. 프로젝트가 커질수록 이런 문제는 눈덩이처럼 불어났습니다.
바로 이런 문제를 해결하기 위해 컴포넌트 기반 설계가 등장했습니다. 컴포넌트를 잘 설계하면 한 곳에서 수정하면 모든 곳에 반영됩니다.
또한 새로운 기능을 추가할 때도 기존 컴포넌트를 조합하면 되니 개발 속도가 빨라집니다. 무엇보다 코드의 일관성이 유지되어 사용자 경험이 좋아진다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ButtonProps 인터페이스를 보면 이 컴포넌트가 받을 수 있는 모든 속성을 정의하고 있습니다.
variant와 size에 물음표가 붙어 있는데, 이는 선택적 속성이라는 뜻입니다. 기본값을 지정해두면 사용하는 쪽에서 매번 전달하지 않아도 됩니다.
**variant = 'primary'**라고 작성한 부분이 기본값 설정입니다. 이렇게 하면 Button 컴포넌트를 사용할 때 variant를 명시하지 않으면 자동으로 primary 스타일이 적용됩니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 서비스를 개발한다고 가정해봅시다.
장바구니 담기 버튼, 구매하기 버튼, 찜하기 버튼 등 수십 개의 버튼이 필요합니다. 이때 잘 설계된 Button 컴포넌트 하나만 있으면 모든 상황에 대응할 수 있습니다.
디자인 시스템을 운영하는 대부분의 기업에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 Props를 만드는 것입니다. 모든 상황을 대비하려다 보면 Props가 20개, 30개로 늘어납니다.
이렇게 하면 오히려 사용하기 어려워집니다. 따라서 정말 필요한 것만 외부에서 제어하도록 설계해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 처음부터 Props를 고려해서 설계해야 하는군요!" 컴포넌트 설계를 제대로 이해하면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - Props의 기본값을 적극 활용하여 사용하기 쉬운 API를 만드세요
- 하나의 컴포넌트는 하나의 역할만 담당하도록 설계하세요
- TypeScript의 interface를 활용해 Props의 타입을 명확히 정의하세요
2. 커스텀 훅 만들기
어느 날 김개발 씨가 코드 리뷰를 받다가 선배에게 이런 말을 들었습니다. "이 로직, 저쪽 컴포넌트에서도 똑같이 쓰고 있던데요?
커스텀 훅으로 분리하면 어떨까요?" 커스텀 훅이라니, 뭔가 대단한 기술처럼 들립니다. 과연 그럴까요?
커스텀 훅은 반복되는 상태 관리 로직을 재사용 가능한 함수로 추출하는 것입니다. 마치 자주 쓰는 요리 레시피를 레시피북에 정리해두는 것처럼, 한 번 만들어두면 어디서든 꺼내 쓸 수 있습니다.
이것을 제대로 활용하면 컴포넌트를 깔끔하게 유지하면서 로직을 공유할 수 있습니다.
다음 코드를 살펴봅시다.
// API 데이터를 가져오는 커스텀 훅
import { useState, useEffect } from 'react';
// 핵심 포인트: use로 시작하는 함수명이 규칙입니다
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// 여기서 실제 데이터 요청이 일어납니다
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
김개발 씨는 사용자 목록을 보여주는 페이지를 만들고 있었습니다. API를 호출하고, 로딩 상태를 관리하고, 에러를 처리하는 코드를 작성했습니다.
그런데 다음 날 상품 목록 페이지를 만들 때, 거의 똑같은 코드를 또 작성하고 있는 자신을 발견했습니다. 박시니어 씨가 다가와 말합니다.
"그 로직, 커스텀 훅으로 빼면 한 번만 작성해도 되는 거 아시죠?" 그렇다면 커스텀 훅이란 정확히 무엇일까요? 쉽게 비유하자면, 커스텀 훅은 마치 요리사의 비법 레시피와 같습니다.
된장찌개를 끓일 때마다 매번 된장의 양, 물의 비율, 끓이는 시간을 고민할 필요가 없습니다. 한 번 완성된 레시피가 있으면 그대로 따라 하기만 하면 됩니다.
커스텀 훅도 마찬가지로, 한 번 검증된 상태 관리 로직을 어디서든 꺼내 쓸 수 있게 해줍니다. 커스텀 훅이 없던 시절에는 어땠을까요?
개발자들은 비슷한 상태 관리 로직이 필요할 때마다 useState와 useEffect를 반복해서 작성했습니다. 10개의 페이지에서 API를 호출하면 10번 같은 코드를 작성했습니다.
더 큰 문제는 하나의 로직에서 버그가 발견되었을 때였습니다. 10군데를 모두 찾아서 수정해야 했고, 이 과정에서 또 다른 실수가 생기기도 했습니다.
바로 이런 문제를 해결하기 위해 커스텀 훅이라는 패턴이 등장했습니다. 커스텀 훅을 사용하면 로직을 한 곳에서 관리할 수 있습니다.
버그가 발견되면 훅 하나만 수정하면 모든 곳에 반영됩니다. 또한 컴포넌트는 UI에만 집중할 수 있어서 코드가 훨씬 깔끔해집니다.
무엇보다 테스트하기도 쉬워집니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 함수 이름이 useFetch로 시작하는 것을 주목하세요. React에서 커스텀 훅은 반드시 use로 시작해야 합니다.
이것은 단순한 관례가 아니라, React가 훅인지 일반 함수인지 구분하는 데 사용하는 규칙입니다. useState로 세 가지 상태를 관리합니다.
data는 실제 데이터, loading은 로딩 중인지 여부, error는 에러 정보를 담습니다. 이 세 가지는 API 호출에서 항상 필요한 상태들입니다.
useEffect 안에서 실제 fetch가 일어납니다. url이 바뀔 때마다 새로운 요청을 보내도록 의존성 배열에 url을 넣었습니다.
마지막으로 객체 형태로 세 상태를 반환하면, 사용하는 쪽에서 필요한 것만 꺼내 쓸 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 대시보드 서비스를 개발한다고 가정해봅시다. 매출 차트, 사용자 통계, 주문 현황 등 여러 API를 호출해야 합니다.
useFetch 훅 하나로 모든 데이터 요청을 일관되게 처리할 수 있습니다. 나중에 캐싱이나 재시도 로직을 추가하더라도 훅 하나만 수정하면 됩니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 복잡한 훅을 만드는 것입니다.
하나의 훅에 여러 가지 기능을 넣다 보면 오히려 이해하기 어려워집니다. 따라서 하나의 훅은 하나의 관심사만 다루도록 설계해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 useFetch 훅을 만들어 적용해봤습니다.
"와, 컴포넌트 코드가 절반으로 줄었어요!" 커스텀 훅을 제대로 활용하면 반복을 줄이고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 여러분도 반복되는 로직이 보이면 커스텀 훅으로 분리하는 습관을 들여보세요.
실전 팁
💡 - 커스텀 훅의 이름은 반드시 use로 시작해야 합니다
- 하나의 훅은 하나의 관심사만 다루도록 작게 유지하세요
- 훅에서 반환하는 값은 객체로 감싸서 필요한 것만 구조 분해할 수 있게 하세요
3. Next.js 설정
김개발 씨는 React로 만든 포트폴리오 사이트가 검색 엔진에 잘 노출되지 않는다는 피드백을 받았습니다. 검색해보니 서버 사이드 렌더링이 필요하다고 합니다.
그러다 발견한 것이 바로 Next.js였습니다. "React인데 서버에서도 돌아간다고요?"
Next.js는 React의 기능을 확장하여 서버 사이드 렌더링, 정적 사이트 생성, API 라우트 등을 제공하는 프레임워크입니다. 마치 자동차에 네비게이션과 후방 카메라가 기본 장착되어 나오는 것처럼, 웹 개발에 필요한 많은 기능이 이미 준비되어 있습니다.
이것을 사용하면 복잡한 설정 없이 강력한 웹 애플리케이션을 만들 수 있습니다.
다음 코드를 살펴봅시다.
// next.config.js - Next.js 기본 설정
/** @type {import('next').NextConfig} */
const nextConfig = {
// 핵심 포인트: 자주 사용하는 설정들입니다
reactStrictMode: true,
// 이미지 최적화를 위한 도메인 설정
images: {
domains: ['example.com', 'cdn.mysite.com'],
},
// 환경 변수 노출 설정
env: {
API_URL: process.env.API_URL,
},
// 리다이렉트 설정
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
];
},
};
module.exports = nextConfig;
김개발 씨는 순수 React로 만든 웹사이트를 가지고 있었습니다. 모든 것이 잘 작동했지만, 한 가지 문제가 있었습니다.
구글에서 검색해도 사이트가 잘 나오지 않았습니다. 왜 그런 걸까요?
박시니어 씨가 설명합니다. "순수 React 앱은 클라이언트에서 렌더링되기 때문에, 검색 엔진 봇이 방문했을 때 빈 HTML만 보입니다.
Next.js를 사용하면 서버에서 미리 렌더링해서 보내줄 수 있어요." 그렇다면 Next.js란 정확히 무엇일까요? 쉽게 비유하자면, Next.js는 마치 풀옵션 자동차와 같습니다.
일반 자동차에 네비게이션, 후방 카메라, 자동 주차 기능을 따로 설치하려면 복잡하고 비용도 많이 듭니다. 하지만 처음부터 풀옵션으로 나온 차를 사면 모든 게 이미 통합되어 있습니다.
Next.js도 마찬가지로, 라우팅, 코드 스플리팅, 이미지 최적화 등이 이미 내장되어 있습니다. Next.js가 없던 시절에는 어땠을까요?
개발자들은 React 앱에 서버 사이드 렌더링을 추가하려면 직접 Node.js 서버를 설정해야 했습니다. 라우팅을 위해 react-router를 설치하고, 코드 스플리팅을 위해 webpack 설정을 만지작거려야 했습니다.
이미지 최적화? 또 다른 라이브러리가 필요했습니다.
프로젝트 시작 전에 이런 설정만 하루 종일 걸리기도 했습니다. 바로 이런 문제를 해결하기 위해 Vercel에서 Next.js를 만들었습니다.
Next.js를 사용하면 파일 기반 라우팅이 자동으로 제공됩니다. pages 폴더에 파일을 만들면 그게 곧 URL이 됩니다.
또한 Image 컴포넌트로 이미지를 자동 최적화할 수 있습니다. 무엇보다 서버 사이드 렌더링과 정적 사이트 생성을 상황에 따라 선택할 수 있습니다.
위의 설정 파일을 살펴보겠습니다. reactStrictMode: true는 개발 중에 잠재적인 문제를 찾아주는 모드입니다.
images.domains는 외부 이미지를 사용할 때 허용할 도메인 목록입니다. 보안상의 이유로 명시적으로 허용해야 합니다.
env는 환경 변수를 클라이언트에서도 사용할 수 있게 해줍니다. redirects 함수는 URL 리다이렉트를 설정합니다.
페이지 경로가 바뀌었을 때 사용자를 자동으로 새 경로로 안내할 수 있습니다. permanent: true는 301 리다이렉트를, false는 302 리다이렉트를 의미합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 블로그 서비스를 개발한다고 가정해봅시다.
블로그 글은 SEO가 중요하므로 서버 사이드 렌더링이 필수입니다. 또한 여러 CDN에서 이미지를 가져오므로 images.domains 설정이 필요합니다.
기존 URL 구조가 바뀌었다면 redirects로 깨진 링크를 방지할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 환경 변수 노출입니다. NEXT_PUBLIC_ 접두사가 붙은 환경 변수는 클라이언트에 노출되므로, API 키 같은 민감한 정보는 절대 이 접두사를 붙이면 안 됩니다.
서버에서만 사용할 변수는 접두사 없이 사용하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.
Next.js로 마이그레이션한 후, 김개발 씨의 포트폴리오 사이트는 구글 검색 결과 첫 페이지에 나타나기 시작했습니다. "이렇게 큰 차이가 나다니!" Next.js를 제대로 설정하면 SEO부터 성능 최적화까지 많은 것을 얻을 수 있습니다.
여러분도 새 프로젝트를 시작할 때 Next.js를 고려해보세요.
실전 팁
💡 - 민감한 환경 변수에는 절대 NEXT_PUBLIC_ 접두사를 붙이지 마세요
- images.domains에 사용할 외부 이미지 도메인을 미리 등록하세요
- redirects를 활용해 URL 변경 시 사용자 경험을 보호하세요
4. SSR vs CSR 고려사항
김개발 씨는 Next.js를 배우면서 새로운 고민에 빠졌습니다. "getServerSideProps를 쓸까, 그냥 useEffect로 데이터를 가져올까?" 둘 다 데이터를 가져오는 건 같은데, 뭐가 다른 걸까요?
그리고 언제 어떤 걸 써야 할까요?
**SSR(Server Side Rendering)**은 서버에서 HTML을 완성해서 보내는 방식이고, **CSR(Client Side Rendering)**은 브라우저에서 JavaScript로 화면을 그리는 방식입니다. 마치 음식점에서 완성된 요리를 받는 것과 밀키트를 받아 직접 조리하는 것의 차이와 같습니다.
상황에 따라 적절한 방식을 선택하면 성능과 사용자 경험을 모두 잡을 수 있습니다.
다음 코드를 살펴봅시다.
// SSR 방식 - 서버에서 데이터를 가져옵니다
export async function getServerSideProps() {
// 핵심 포인트: 이 코드는 서버에서만 실행됩니다
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts }, // 페이지 컴포넌트에 전달됩니다
};
}
// CSR 방식 - 브라우저에서 데이터를 가져옵니다
function PostsPage() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// 여기서 클라이언트 측 데이터 요청이 일어납니다
fetch('/api/posts')
.then(res => res.json())
.then(data => setPosts(data));
}, []);
return <PostList posts={posts} />;
}
김개발 씨는 블로그 플랫폼을 만들고 있었습니다. 글 목록 페이지를 구현하는데, 두 가지 방법이 있다는 걸 알게 되었습니다.
하나는 서버에서 데이터를 미리 가져와서 HTML에 포함시키는 것이고, 다른 하나는 빈 페이지를 먼저 보여주고 JavaScript로 데이터를 나중에 불러오는 것입니다. 박시니어 씨가 칠판에 그림을 그리며 설명합니다.
"이게 바로 SSR과 CSR의 차이입니다. 둘 다 장단점이 있어서, 상황에 맞게 선택해야 해요." 그렇다면 SSR과 CSR이란 정확히 무엇일까요?
쉽게 비유하자면, SSR은 마치 레스토랑에서 완성된 요리를 받는 것과 같습니다. 주방에서 모든 조리가 끝난 상태로 테이블에 도착합니다.
바로 먹을 수 있죠. 반면 CSR은 밀키트를 받아 직접 조리하는 것과 같습니다.
재료와 레시피가 도착하면, 집에서 직접 요리해야 먹을 수 있습니다. 두 방식은 왜 생겨났을까요?
초창기 웹은 모두 SSR이었습니다. 서버에서 HTML을 완성해서 보내줬죠.
하지만 페이지가 복잡해지고 상호작용이 많아지면서 CSR이 등장했습니다. 전체 페이지를 새로고침하지 않고도 일부분만 업데이트할 수 있게 된 것입니다.
그러다 SEO 문제가 불거지면서, 다시 SSR의 장점이 주목받기 시작했습니다. SSR의 장점은 무엇일까요?
첫째, **검색 엔진 최적화(SEO)**에 유리합니다. 검색 엔진 봇이 페이지를 방문했을 때 완성된 콘텐츠를 볼 수 있습니다.
둘째, 초기 로딩 속도가 빠릅니다. JavaScript를 다운로드하고 실행할 때까지 기다리지 않아도 됩니다.
특히 느린 네트워크 환경에서 차이가 큽니다. 반면 CSR의 장점은 무엇일까요?
첫째, 상호작용이 빠릅니다. 페이지 간 이동 시 전체를 새로 그리지 않고 필요한 부분만 업데이트합니다.
둘째, 서버 부하가 적습니다. 렌더링 작업을 클라이언트가 담당하기 때문입니다.
셋째, 사용자 기기의 성능이 좋아진 요즘, 오히려 CSR이 더 부드러운 경험을 제공하기도 합니다. 그렇다면 언제 어떤 걸 선택해야 할까요?
SSR을 선택해야 하는 경우가 있습니다. 블로그, 뉴스 사이트처럼 SEO가 중요한 페이지입니다.
소셜 미디어에 공유될 때 미리보기가 제대로 나와야 하는 페이지도 마찬가지입니다. 초기 로딩 속도가 비즈니스에 직접적인 영향을 주는 경우도 SSR이 좋습니다.
CSR을 선택해야 하는 경우도 있습니다. 대시보드, 관리자 페이지처럼 로그인 후에만 접근하는 페이지입니다.
검색 엔진에 노출될 필요가 없으니까요. 실시간으로 데이터가 자주 바뀌는 페이지도 CSR이 적합합니다.
차트나 그래프처럼 복잡한 상호작용이 많은 경우도 마찬가지입니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 모든 페이지에 SSR을 적용하는 것입니다. SSR은 매 요청마다 서버에서 렌더링을 수행하므로 서버 비용이 증가합니다.
또한 SSR 함수 안에서 시간이 오래 걸리는 작업을 하면, 사용자가 빈 화면을 오래 보게 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
고민 끝에 김개발 씨는 블로그 글 목록과 본문은 SSR로, 댓글과 좋아요는 CSR로 구현하기로 했습니다. "둘 다 적절히 섞어서 쓰면 되는군요!" SSR과 CSR의 특성을 이해하면 각 상황에 맞는 최적의 선택을 할 수 있습니다.
여러분도 페이지의 특성을 고려해서 적절한 렌더링 전략을 세워보세요.
실전 팁
💡 - SEO가 중요한 공개 페이지는 SSR, 로그인 후 페이지는 CSR을 고려하세요
- Next.js의 getStaticProps를 활용하면 빌드 시점에 미리 렌더링할 수도 있습니다
- 하이브리드 방식으로 페이지마다 다른 렌더링 전략을 적용하세요
5. 로딩 상태 관리
김개발 씨가 만든 페이지에서 문제가 발생했습니다. 버튼을 클릭했는데 아무 반응이 없습니다.
알고 보니 데이터를 가져오는 중이었는데, 사용자에게 아무런 표시가 없었던 것입니다. 사용자들은 버튼이 고장난 줄 알고 여러 번 클릭했고, 결국 중복 요청이 발생했습니다.
로딩 상태 관리는 비동기 작업이 진행 중임을 사용자에게 알려주는 것입니다. 마치 엘리베이터의 층수 표시등처럼, 현재 어떤 상태인지 보여주면 사용자는 안심하고 기다릴 수 있습니다.
이것을 제대로 구현하면 사용자 경험이 크게 향상되고 중복 요청 같은 문제도 방지할 수 있습니다.
다음 코드를 살펴봅시다.
// 로딩 상태를 관리하는 컴포넌트
import { useState, useTransition } from 'react';
function SubmitButton({ onSubmit }: { onSubmit: () => Promise<void> }) {
const [isPending, startTransition] = useTransition();
const [isSubmitting, setIsSubmitting] = useState(false);
// 핵심 포인트: 로딩 중에는 버튼을 비활성화합니다
const handleClick = async () => {
if (isSubmitting) return; // 중복 클릭 방지
setIsSubmitting(true);
try {
await onSubmit();
} finally {
setIsSubmitting(false);
}
};
return (
<button
onClick={handleClick}
disabled={isSubmitting}
className={isSubmitting ? 'opacity-50 cursor-not-allowed' : ''}
>
{isSubmitting ? '처리 중...' : '제출하기'}
</button>
);
}
김개발 씨는 회원가입 폼을 만들었습니다. 폼을 제출하면 서버에 데이터를 보내고, 성공하면 다음 페이지로 이동합니다.
그런데 문제가 생겼습니다. 서버 응답이 늦어질 때 사용자들이 버튼을 여러 번 클릭했고, 같은 데이터가 중복으로 등록되는 사고가 발생한 것입니다.
박시니어 씨가 한숨을 쉬며 말합니다. "로딩 상태를 표시하지 않으면 이런 일이 생기죠.
사용자는 지금 뭐가 일어나고 있는지 전혀 모르니까요." 그렇다면 로딩 상태 관리란 정확히 무엇일까요? 쉽게 비유하자면, 로딩 상태 관리는 마치 엘리베이터의 층수 표시등과 같습니다.
버튼을 누르면 현재 몇 층에 있는지, 올라가고 있는지 내려가고 있는지 표시됩니다. 이 표시가 없다면 사람들은 엘리베이터가 오고 있는지, 고장난 건지 알 수 없어서 불안해할 것입니다.
웹 애플리케이션도 마찬가지입니다. 로딩 상태를 표시하지 않으면 어떤 문제가 생길까요?
첫째, 사용자가 버튼이 작동하지 않는다고 착각합니다. 클릭했는데 아무 반응이 없으면 "어?
안 눌렸나?" 하고 다시 누릅니다. 둘째, 중복 요청이 발생합니다.
서버에 같은 요청이 여러 번 가면 데이터 중복, 과도한 비용 청구 등 심각한 문제가 생길 수 있습니다. 셋째, 사용자의 신뢰가 떨어집니다.
반응이 없는 서비스는 불안하게 느껴지니까요. 바로 이런 문제를 해결하기 위해 로딩 상태 관리가 필요합니다.
로딩 상태를 표시하면 사용자는 "아, 지금 처리 중이구나" 하고 기다릴 수 있습니다. 버튼을 비활성화하면 중복 클릭도 원천적으로 막을 수 있습니다.
무엇보다 전문적이고 신뢰할 수 있는 서비스라는 인상을 줍니다. 위의 코드를 살펴보겠습니다.
isSubmitting 상태 변수로 현재 제출 중인지를 추적합니다. handleClick 함수 시작 부분에서 이미 제출 중이면 바로 리턴합니다.
이것이 중복 클릭을 막는 첫 번째 방어선입니다. try-finally 구문을 사용한 것을 주목하세요.
성공하든 실패하든 finally 블록이 실행되어 isSubmitting을 false로 되돌립니다. 만약 에러가 발생해도 버튼이 영원히 비활성화되는 일을 방지합니다.
버튼의 disabled 속성과 스타일 변경도 중요합니다. disabled만으로도 클릭이 막히지만, 시각적으로도 비활성화 상태임을 보여줘야 합니다.
흐릿한 색상과 cursor-not-allowed로 명확하게 표시합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 결제 시스템을 개발한다고 가정해봅시다. 결제 버튼은 절대로 중복 클릭되면 안 됩니다.
한 번의 주문에 두 번 결제되면 큰 문제가 됩니다. 이때 로딩 상태 관리는 선택이 아닌 필수입니다.
많은 이커머스 사이트에서 결제 버튼을 누르면 "결제 처리 중..." 메시지와 함께 스피너가 돌아가는 걸 볼 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 로딩 상태를 해제하지 않는 것입니다. 에러가 발생했을 때 finally 블록이 없으면 버튼이 영원히 비활성화 상태로 남습니다.
사용자는 페이지를 새로고침하지 않으면 아무것도 할 수 없게 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
로딩 상태 관리를 추가한 후, 중복 가입 문제는 완전히 사라졌습니다. 사용자들도 "처리 중"이라는 표시를 보고 안심하고 기다리게 되었습니다.
로딩 상태 관리를 제대로 구현하면 더 안정적이고 신뢰할 수 있는 서비스를 만들 수 있습니다. 여러분도 비동기 작업에는 반드시 로딩 상태를 표시하세요.
실전 팁
💡 - try-finally 패턴으로 에러 상황에서도 로딩 상태를 해제하세요
- 버튼의 텍스트를 "제출" → "처리 중..."으로 바꿔 상태를 명확히 하세요
- 네트워크가 느린 환경을 시뮬레이션해서 테스트하세요
6. 에러 바운더리 처리
금요일 오후, 김개발 씨의 앱에서 갑자기 흰 화면이 나타났습니다. 콘솔을 보니 어떤 컴포넌트에서 undefined의 속성에 접근하려다 에러가 발생한 것입니다.
그런데 왜 전체 앱이 다 멈춰버린 걸까요? 에러가 난 건 작은 위젯 하나뿐인데 말입니다.
에러 바운더리는 하위 컴포넌트 트리에서 발생한 에러를 잡아서 대체 UI를 표시하는 기능입니다. 마치 전기 회로의 차단기가 일부 회로만 끊어서 전체 화재를 막는 것처럼, 앱의 일부에서 에러가 나도 전체가 죽지 않도록 보호합니다.
이것을 사용하면 사용자에게 훨씬 나은 에러 경험을 제공할 수 있습니다.
다음 코드를 살펴봅시다.
// 에러 바운더리 컴포넌트 구현
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
}
interface State {
hasError: boolean;
}
// 핵심 포인트: 클래스 컴포넌트로 구현해야 합니다
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
// 여기서 에러를 감지합니다
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('에러 발생:', error, errorInfo);
// 에러 리포팅 서비스로 전송할 수 있습니다
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
김개발 씨는 금요일 저녁을 앞두고 있었습니다. 한 주의 마무리를 잘하고 퇴근하려던 찰나, 고객 지원팀에서 연락이 왔습니다.
"앱이 흰 화면만 나와요." 확인해보니 작은 날씨 위젯에서 API 응답이 예상과 달라 에러가 발생했고, 그 여파로 전체 앱이 멈춰버린 것입니다. 박시니어 씨가 커피를 건네며 말합니다.
"에러 바운더리를 적용했으면 이런 일은 없었을 텐데요. 날씨 위젯만 에러 메시지를 보여주고, 나머지는 정상 작동했을 거예요." 그렇다면 에러 바운더리란 정확히 무엇일까요?
쉽게 비유하자면, 에러 바운더리는 마치 건물의 방화문과 같습니다. 한 방에서 불이 나도 방화문이 닫혀 있으면 다른 방으로 번지지 않습니다.
마찬가지로 에러 바운더리가 있으면 한 컴포넌트의 에러가 전체 앱을 망가뜨리지 않습니다. 에러가 발생한 부분만 대체 UI를 보여주고, 나머지는 정상 작동합니다.
에러 바운더리가 없으면 어떤 일이 벌어질까요? React에서 컴포넌트 렌더링 중 에러가 발생하면, 기본적으로 전체 컴포넌트 트리가 언마운트됩니다.
작은 버튼 하나에서 에러가 나도 화면 전체가 흰색으로 변합니다. 개발 환경에서는 에러 오버레이가 뜨지만, 프로덕션에서는 그냥 빈 화면입니다.
사용자 입장에서는 앱이 완전히 고장난 것처럼 보입니다. 바로 이런 문제를 해결하기 위해 React는 에러 바운더리 기능을 제공합니다.
에러 바운더리를 사용하면 에러가 발생한 영역만 대체 UI로 교체됩니다. "문제가 발생했습니다.
다시 시도해주세요." 같은 메시지를 보여줄 수 있습니다. 또한 componentDidCatch에서 에러 정보를 로깅 서비스로 보내서 나중에 분석할 수도 있습니다.
위의 코드를 살펴보겠습니다. 에러 바운더리는 클래스 컴포넌트로만 구현할 수 있습니다.
이것은 React의 제약 사항입니다. getDerivedStateFromError는 하위 컴포넌트에서 에러가 발생했을 때 호출됩니다.
여기서 hasError를 true로 바꿔서 대체 UI를 보여주도록 합니다. componentDidCatch에서는 에러 정보를 받아볼 수 있습니다.
보통 이 정보를 Sentry나 LogRocket 같은 에러 리포팅 서비스로 전송합니다. 이렇게 하면 사용자가 겪은 에러를 개발팀이 바로 파악할 수 있습니다.
render 메서드에서는 hasError 상태에 따라 fallback 또는 children을 렌더링합니다. fallback은 에러가 발생했을 때 보여줄 대체 UI이고, children은 정상 상태에서 보여줄 실제 컴포넌트입니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 소셜 미디어 피드를 개발한다고 가정해봅시다.
각 게시물 카드를 에러 바운더리로 감쌀 수 있습니다. 한 게시물의 데이터가 잘못되어도 그 카드만 "표시할 수 없는 게시물입니다"라고 나오고, 다른 게시물들은 정상적으로 보입니다.
사용자는 앱이 고장났다고 느끼지 않습니다. 하지만 주의할 점도 있습니다.
에러 바운더리는 렌더링 중 발생하는 에러만 잡습니다. 이벤트 핸들러나 비동기 코드에서 발생하는 에러는 잡지 못합니다.
그런 경우에는 try-catch를 직접 사용해야 합니다. 또한 에러 바운더리를 너무 높은 곳에만 두면, 작은 에러에도 큰 영역이 대체 UI로 바뀌어버립니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 주요 컴포넌트마다 에러 바운더리를 적용한 후, 비슷한 문제가 발생해도 해당 영역만 에러 메시지를 보여주게 되었습니다.
"이제 금요일에 전화 받을 일이 줄어들겠네요!" 에러 바운더리를 제대로 활용하면 더 견고하고 사용자 친화적인 앱을 만들 수 있습니다. 여러분도 중요한 컴포넌트에는 에러 바운더리를 적용해보세요.
실전 팁
💡 - 에러 바운더리는 클래스 컴포넌트로만 구현할 수 있습니다
- 너무 높은 곳 하나가 아닌, 적절한 수준에 여러 개를 배치하세요
- componentDidCatch에서 에러 리포팅 서비스로 로그를 전송하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Docker 실전 프로젝트 완벽 가이드
프론트엔드, 백엔드, 데이터베이스를 Docker로 컨테이너화하고 Nginx 리버스 프록시를 구성하여 실제 프로덕션 환경에 배포하는 전 과정을 다룹니다. 실무에서 바로 적용할 수 있는 Docker Compose 기반 멀티 컨테이너 애플리케이션 구축법을 배웁니다.
Docker Compose 실전 활용 완벽 가이드
Docker Compose를 활용하여 복잡한 멀티 컨테이너 환경을 손쉽게 구축하고 관리하는 방법을 배웁니다. 개발 환경부터 운영 환경까지, 실무에서 바로 적용할 수 있는 핵심 기법들을 단계별로 알아봅니다.
API 표준과 규정 준수 완벽 가이드
현대 웹 서비스에서 필수적인 개인정보 보호와 API 보안에 대해 알아봅니다. GDPR부터 데이터 암호화, 감사 로그까지 실무에서 반드시 알아야 할 규정 준수 방법을 초보자도 이해할 수 있게 설명합니다.
API 아키텍처 패턴과 설계 완벽 가이드
모놀리식부터 마이크로서비스까지, 현대 백엔드 아키텍처의 핵심 패턴들을 초급 개발자도 이해할 수 있도록 실무 예제와 함께 설명합니다. API Gateway, BFF, 이벤트 기반 아키텍처 등 실전에서 바로 적용할 수 있는 설계 전략을 다룹니다.
OpenAPI/Swagger로 API 문서화 완벽 가이드
API 문서화의 표준인 OpenAPI와 Swagger를 활용하여 프론트엔드 개발자와 원활하게 협업하는 방법을 배웁니다. 스펙 작성부터 자동 문서 생성, 버전 관리까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.