Yasin Ateş

Component Driven Development Nedir? Nasıl Uygulanır?

Component Driven Development Nedir? Nasıl Uygulanır?

Sayfadan başlayıp component’lere inmek yerine, component’lerdan başlayıp sayfaya doğru çıkıyoruz. Bu yazıda Component-Driven Development (CDD) yaklaşımını React ile ele alacağız. Bir blog sitesi örneği üzerinden component’ları nasıl izole geliştireceğimizi, Storybook ile nasıl birlikte kullanacağımız değineceğim

CDD Nedir ve Neden Tercih Ediyoruz? 🤔

Şöyle bir senaryo düşünelim: Yeni bir blog projesine başlıyoruz. Tasarımcı Figma’dan bir sayfa atıyor, HomePage.jsx dosyasını açıyoruz. Header’ı yazıyoruz, altına yazı kartlarını ekliyoruz, sidebar’ı yerleştiriyoruz. Birkaç gün sonra kategori sayfasında da aynı yazı kartlarına ihtiyaç duyuyoruz. Ne yapıyoruz? Kopyala yapıştır. 😅

İşte tam bu noktada işler çığırından çıkmaya başlıyor.

Component-Driven Development (CDD), uygulamayı sayfadan başlayarak değil, en küçük UI parçalarından başlayarak inşa etme yaklaşımı. Yani önce sayfayı değil, sayfayı oluşturacak bağımsız ve yeniden kullanılabilir (reusable) parçaları geliştiriyoruz. Sonra bu parçaları birleştirerek büyük yapılar kuruyoruz.

Büyüyen projelerde şu cümleleri duymak kaçınılmaz oluyor:

  • “Bu buton neden burada farklı görünüyor?”
  • “Modalın 4 tane versiyonu var, hangisi doğru?”
  • “Bu form validation her sayfada başka türlü çalışıyor”
  • “Refactor yapmaya çekiniyoruz, neresi patlar bilinmez”

CDD bu sorunların önüne geçmek için şunu söylüyor: Önce parçaları yap, sonra birleştir.

CDD ile componentleri parçalama

Birbirine karışan birkaç terimi de netleştirelim:

  • CDD: UI tarafında component’ların izolasyon sağlanarak geliştirilmesi, varyantlarının çıkarılması, dokümantasyon pratikleriyle bir geliştirme yaklaşımı.
  • CBD (Component-Based Development): Daha geniş bir terim. UI ile sınırlı değil, yazılımı component’lere bölerek geliştirme fikrini kapsar.
  • Design System: Tasarım kararlarının (renk, typography, spacing) ve UI component’lerin bir standarda bağlanmış hali. CDD çoğu zaman design system pratikleriyle el ele gider.
  • Atomic Design: CDD’nin “parçalara böl” fikrini düzenli hale getiren bir metodoloji. Bu konuyu detaylıca işlediğim ayrı bir yazı var, oraya göz atabilirsiniz.

🧱 CDD’nin Temel Prensipleri

CDD sadece Storybook kurmak değil, asıl konu mimariyi doğru kurgulamak;

1) Component API’si bir contract’tır

Component’in props’ları bizim public API’miz. Bugün Button component’ine verilen kararlar, yarın 40 ekranda karşımıza çıkıyor. Props isimleri net olmalı, varyant sayısı sınırlı olmalı ve default davranış her zaman tahmin edilebilir olmalı.

Şu soruları sormadan component’i “tamam” saymıyoruz:

  • Bu component’in sorumluluğu tek mi?
  • Props isimleri net mi?
  • Varyantlar sınırlı ve anlamlı mı?
  • Default davranış öngörülebilir mi?

2) State’ler ve varyant isimleri mantıklı olmalı

CDD’nin en güzel kazanımı şu: component’i sadece “happy path” ile bırakmıyoruz. Her component’in şu durumlarını düşünmemiz gerekiyor:

  • loading
  • disabled
  • error
  • empty
  • Uzun metin / kısa metin
  • İkon + metin
  • Klavye ile kullanım ve erişilebilirlik ayarları

3) Önce izolasyon, sonra kompozisyon

Önce component’i tek başına olgunlaştırıyoruz, sonra ekranlara birleştiriyoruz. Kulağa basit geliyor; ama uygulamada bu alışkanlığı edinmek biraz zaman alıyor. Veriyi sayfalar yönetmeli, component’lar sadece render etmeli.

🛠️ React ile CDD: Blog Sitesi Örneği

Düşünelim ki, yazıları listeleyen, her yazı için kategori etiketi ve okuma süresi gösteren bir sayfa isteniyor. Bunu CDD yaklaşımıyla küçük parçalardan başlayarak inşa edelim.

Kullanacağımız component’lar şöyle:

  • Tag → Kategori etiketi
  • ReadingTime → Okuma süresi göstergesi
  • PostCard → Ana blog kartı
  • PostGrid → Kartları listeleyen grid component’i

Her component’i kendi klasöründe tutuyoruz; story’si de yanında. Bu yaklaşım refactor ve taşıma işini çok kolaylaştırıyor:

src/
 components/
 Tag/
 Tag.tsx
 Tag.stories.tsx
 index.ts
 ReadingTime/
 ReadingTime.tsx
 ReadingTime.stories.tsx
 index.ts
 PostCard/
 PostCard.tsx
 PostCard.stories.tsx
 index.ts
 PostGrid/
 PostGrid.tsx
 PostGrid.stories.tsx
 index.ts
 pages/
 BlogPage/
 BlogPage.tsx

Adım 1: En küçük, bağımsız parçaları yazalım

En küçük parçalardan başlıyoruz. Bu component’lar dışarıya bağımlı değil; sadece kendilerine ne verilirse onu gösteriyorlar.

Tag component'ını yazalım. Yazı kategorisini göstermek için kullanacağız:

// src/components/Tag/Tag.tsx
type TagVariant = 'react' | 'typescript' | 'css' | 'career' | 'default';

type TagProps = {
 variant?: TagVariant;
 children: React.ReactNode;
};

function Tag({ variant = 'default', children }: TagProps) {
 const colors: Record<TagVariant, { bg: string; text: string }> = {
 react: { bg: '#61dafb22', text: '#61dafb' },
 typescript: { bg: '#3178c622', text: '#3178c6' },
 css: { bg: '#a78bfa22', text: '#a78bfa' },
 career: { bg: '#f472b622', text: '#f472b6' },
 default: { bg: '#6b728022', text: '#6b7280' },
 };

return (
 <span
 style={{
 backgroundColor: colors[variant].bg,
 color: colors[variant].text,
 padding: '3px 10px',
 borderRadius: '9999px',
 fontSize: '12px',
 fontWeight: '600',
 border: `1px solid ${colors[variant].text}44`,
 }}
 >
 {children}
 </span>
 );
}

export default Tag;
  • variant prop'u ile hem arkaplan hem de yazı rengi dışarıdan kontrol ediliyor.
  • Component hiçbir global state’e ya da context’e bağımlı değil. Tamamen izole.

Şimdi ReadingTime component'ını yazalım:

// src/components/ReadingTime/ReadingTime.tsx
type ReadingTimeProps = {
 minutes: number;
};

function ReadingTime({ minutes }: ReadingTimeProps) {
 const label = minutes === 1 ? '1 dk okuma' : `${minutes} dk okuma`;

 return (
 <span
 style={{
 color: '#9ca3af',
 fontSize: '13px',
 display: 'flex',
 alignItems: 'center',
 gap: '4px',
 }}
 >
 🕐 {label}
 </span>
 );
}

export default ReadingTime;
  • Tek bir prop alıyor, tek bir şey gösteriyor. Sorumluluğu net ve dar.
  • Tekil/çoğul ayrımını component içinde hallettik. Kullanan yerin bunu düşünmesine gerek yok.

Adım 2: Parçaları birleştiren component’ı yazalım

Küçük parçalarımız hazır. Şimdi bunları bir araya getiren PostCard component'ını oluşturalım:

// src/components/PostCard/PostCard.tsx
import Tag from '../Tag/Tag';
import ReadingTime from '../ReadingTime/ReadingTime';

type TagVariant = 'react' | 'typescript' | 'css' | 'career' | 'default';

type Post = {
 id: number;
 title: string;
 excerpt: string;
 coverImage: string;
 category: string;
 categoryVariant: TagVariant;
 readingTime: number;
 publishedAt: string;
};

function PostCard({ post }: { post: Post }) {
 const { title, excerpt, coverImage, category, categoryVariant, readingTime, publishedAt } = post;

 return (
 <article
 style={{
 border: '1px solid #1e293b',
 borderRadius: '16px',
 overflow: 'hidden',
 display: 'flex',
 flexDirection: 'column',
 background: '#0f172a',
 transition: 'transform 0.2s ease',
 }}
 >
 <img
 src={coverImage}
 alt={title}
 style={{ width: '100%', height: '200px', objectFit: 'cover' }}
 />
 <div
 style={{
 padding: '20px',
 display: 'flex',
 flexDirection: 'column',
 gap: '10px',
 flex: 1,
 }}
 >
 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
 <Tag variant={categoryVariant}>{category}</Tag>
 <ReadingTime minutes={readingTime} />
 </div>
 <h2 style={{ margin: 0, fontSize: '18px', lineHeight: '1.4', color: '#f1f5f9' }}>
 {title}
 </h2>
 <p style={{ margin: 0, color: '#94a3b8', fontSize: '14px', lineHeight: '1.6' }}>
 {excerpt}
 </p>
 <span style={{ fontSize: '12px', color: '#475569', marginTop: 'auto' }}>
 {publishedAt}
 </span>
 </div>
 </article>
 );
}

export default PostCard;
  • PostCard dışarıdan bir post nesnesi alıyor. Veriyi kendisi fetch etmiyor; bu onun izole kalmasının göstergesi.
  • Kategori ve okuma süresi görselliği alt component’lara devredilmiş. PostCard sadece kompozisyonu yönetiyor.
  • categoryVariant ayrı bir prop olarak geliyor; bu sayede aynı veri tipini farklı stil varyantlarıyla kullanabiliyoruz.

Adım 3: Grid component’ını yazalım

Birden fazla PostCard'ı bir arada gösteren PostGrid component'ını yazalım:

// src/components/PostGrid/PostGrid.tsx
import PostCard from '../PostCard/PostCard';

type TagVariant = 'react' | 'typescript' | 'css' | 'career' | 'default';

type Post = {
 id: number;
 title: string;
 excerpt: string;
 coverImage: string;
 category: string;
 categoryVariant: TagVariant;
 readingTime: number;
 publishedAt: string;
};

function PostGrid({ posts, columns = 3 }: { posts: Post[]; columns?: number }) {
 if (!posts || posts.length === 0) {
 return (
 <div
 style={{
 textAlign: 'center',
 padding: '60px 20px',
 color: '#475569',
 fontSize: '16px',
 }}
 >
 Henüz yayınlanmış yazı bulunamadı.
 </div>
 );
 }

return (
 <div
 style={{
 display: 'grid',
 gridTemplateColumns: `repeat(${columns}, 1fr)`,
 gap: '24px',
 }}
 >
 {posts.map((post) => (
 <PostCard key={post.id} post={post} />
 ))}
 </div>
 );
}

export default PostGrid;
  • columns prop'u ile grid yapısı dışarıdan kontrol edilebiliyor. Mobil için 1, tablet için 2, masaüstü için 3 kolon geçebiliriz.
  • Boş state’i burada yönetiyoruz. Sayfa component’ının bununla ilgilenmesine gerek kalmıyor.

Adım 4: Sayfayı birleştirelim

Son olarak sayfayı oluşturuyoruz. Dikkat edelim: sayfa artık sadece organize ediyor. Tüm iş mantığı aşağıdaki component’larda hallolmuş durumda.

// src/pages/BlogPage/BlogPage.tsx
import { useState, useEffect } from 'react';
import PostGrid from '../../components/PostGrid/PostGrid';

function BlogPage() {
 const [posts, setPosts] = useState([]);
 const [loading, setLoading] = useState(true);

 useEffect(() => {
 // Gerçek projede burası API'den gelen result olurdu
 const mockPosts = [
 {
 id: 1,
 title: 'Vite ile React Projenizi Hızlandırın',
 excerpt: "Webpack'ten Vite'a geçişin avantajlarını ve dikkat edilmesi gereken noktaları ele alıyoruz.",
 coverImage: '/images/vite.jpg',
 category: 'Araçlar',
 categoryVariant: 'react',
 readingTime: 5,
 publishedAt: '12 Temmuz 2025',
 },
 {
 id: 2,
 title: 'TypeScript ile Daha Güvenli Kod',
 excerpt: 'Type safety neden önemli ve günlük geliştirme pratiğine nasıl entegre edilir?',
 coverImage: '/images/typescript.jpg',
 category: 'TypeScript',
 categoryVariant: 'typescript',
 readingTime: 8,
 publishedAt: '5 Temmuz 2025',
 },
 {
 id: 3,
 title: 'CSS-in-JS mi, Tailwind mi?',
 excerpt: 'İki popüler stil yaklaşımını gerçek bir projede yan yana inceliyoruz.',
 coverImage: '/images/css.jpg',
 category: 'CSS',
 categoryVariant: 'css',
 readingTime: 6,
 publishedAt: '28 Haziran 2025',
 },
 ];

 setTimeout(() => {
 setPosts(mockPosts);
 setLoading(false);
 }, 600);
 }, []);

 if (loading) {
 return (
 <div style={{ textAlign: 'center', padding: '80px', color: '#94a3b8' }}>
 Yükleniyor...
 </div>
 );
 }

 return (
 <main style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 20px' }}>
 <h1 style={{ color: '#f1f5f9', marginBottom: '32px' }}>Blog</h1>
 <PostGrid posts={posts} columns={3} />
 </main>
 );
}

export default BlogPage;
  • BlogPage sadece veriyi fetch edip PostGrid'e geçiriyor. Başka bir sorumluluğu yok. Bu Single Responsibility Principle'ın güzel bir yansıması.
  • Loading state’i sayfa seviyesinde yönetiyoruz çünkü veri nereden geldiğini bilen tek yer burası.

📚 Storybook ile İzole Component Geliştirme

CDD’yi benimsediyseniz hayatınıza mutlaka Storybook girmelidir. Storybook, component’larınızı uygulamadan bağımsız olarak geliştirmenizi, test etmenizi ve dokümante etmenizi sağlayan açık kaynaklı bir araç.

Tag component'ını test etmek için her seferinde uygulamayı açıp kategori bilgisi olan bir yazı bulmak istemeyiz. Storybook ile Tag'i izole açıp tüm varyantlarını bir arada görebiliriz.

Storybook ile component geliştirme

Storybook’u kuralım

Önce Vite ile temiz bir React + TypeScript projesi başlatalım:

npm create vite@latest cdd-blog -- --template react-ts
cd cdd-blog
npm install

Şimdi Storybook’u ekleyelim:

npx storybook@latest init
npm run storybook
  • storybook init proje yapına göre gerekli config dosyalarını otomatik ekliyor.
  • npm run storybook ile http://localhost:6006 adresinde component lab ayağa kalkıyor.

Config dosyasına da göz atalım:

// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
 framework: "@storybook/react-vite",
 stories: ["../src/**/*.stories.@(ts|tsx)"],
 addons: [
 "@storybook/addon-essentials",
 "@storybook/addon-interactions",
 "@storybook/addon-a11y",
 ],
};

export default config;
  • addon-essentials controls, actions ve docs gibi temel özellikleri bir arada getiriyor.
  • addon-a11y erişilebilirlik kontrollerini Storybook paneline taşıyor. Bunu ayrıca yüklemek zorunda değilsiniz; bu config ile zaten dahil.

Tag component’ı için story yazalım

// src/components/Tag/Tag.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import Tag from "./Tag";

const meta: Meta<typeof Tag> = {
 title: "Components/Tag",
 component: Tag,
 tags: ["autodocs"],
 argTypes: {
 variant: {
 control: "select",
 options: ["react", "typescript", "css", "career", "default"],
 description: "Tag renk varyantı",
 },
 },
};
export default meta;

type Story = StoryObj<typeof Tag>;

export const ReactTag: Story = {
 args: { variant: "react", children: "React" },
};

export const TypeScriptTag: Story = {
 args: { variant: "typescript", children: "TypeScript" },
};

export const CSSTag: Story = {
 args: { variant: "css", children: "CSS" },
};

// Tüm varyantları bir arada gösteren story
export const TumVariantlar: Story = {
 render: () => (
 <div style={{ display: "flex", gap: "8px", flexWrap: "wrap", padding: "16px" }}>
 <Tag variant="react">React</Tag>
 <Tag variant="typescript">TypeScript</Tag>
 <Tag variant="css">CSS</Tag>
 <Tag variant="career">Kariyer</Tag>
 <Tag variant="default">Genel</Tag>
 </div>
 ),
};
  • tags: ["autodocs"] ile Storybook otomatik olarak prop tablosu içeren bir dokümantasyon sayfası oluşturuyor.
  • Her named export bağımsız bir senaryo. TumVariantlar story'si "tek bakışta her şeyi gör" ihtiyacını karşılıyor.

Şimdi PostCard için de story yazalım

// src/components/PostCard/PostCard.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import PostCard from "./PostCard";

const meta: Meta<typeof PostCard> = {
 title: "Components/PostCard",
 component: PostCard,
 tags: ["autodocs"],
};
export default meta;

type Story = StoryObj<typeof PostCard>;

const mockPost = {
 id: 1,
 title: "Vite ile React Projenizi Hızlandırın",
 excerpt:
 "Webpack'ten Vite'a geçişin avantajlarını ve dikkat edilmesi gereken noktaları ele alıyoruz.",
 coverImage: "https://via.placeholder.com/400x200/0f172a/61dafb?text=Vite",
 category: "React",
 categoryVariant: "react" as const,
 readingTime: 5,
 publishedAt: "12 Temmuz 2025",
};

// Normal durum
export const Varsayilan: Story = {
 args: { post: mockPost },
};

// Uzun başlık edge case
export const UzunBaslik: Story = {
 args: {
 post: {
 ...mockPost,
 title:
 "Bu çok uzun bir başlık ve PostCard component'i bunu nasıl ele aldığını ve satır kırılmasını nasıl yönettiğini görmek istiyoruz",
 },
 },
};

// Çok kısa okuma süresi
export const BirDakika: Story = {
 args: {
 post: { ...mockPost, readingTime: 1 },
 },
};
// Farklı kategori varyantı
export const TypeScriptKategorisi: Story = {
 args: {
 post: {
 ...mockPost,
 category: "TypeScript",
 categoryVariant: "typescript" as const,
 },
 },
};
  • mockPost nesnesini bir kez tanımlayıp spread operatörüyle farklılaştırıyoruz. Story'ler sade ve okunabilir kalıyor.
  • Her story bir senaryoyu temsil ediyor. Uygulamayı açmaya gerek kalmadan tüm edge case’leri kontrol edebiliyoruz.

🧭 Design System ve Monorepo ilişkisi

CDD küçük projelerde de kullanabiliriz ama bana kalırsa asıl güç, ekip büyüyünce ortaya çıkıyor.

Component library ne zaman ayrılmalı?

Erken aşamada ayrı bir library yapmak paket yönetimiyle uğraştırıyor. Önce kullanım şekli olgunlaşsın. Sonra ihtiyaç olunca @myblog/ui gibi ayrı bir package yapabiliriz

Monorepo ne zaman mantıklı?

Bir uygulama varsa monorepo şart değil. İki veya daha fazla uygulama aynı UI’ı paylaşıyorsa Turborepo ciddi bir rahatlık sağlıyor. Bunu detaylıca ele aldığım monorepo yazısına bakabilirsiniz; burada tekrar anlatmayacağım.

🔗 CDD’nin Atomic Design ile Farkı ve Kesişimi

CDD ve Atomic Design

Bu yazıda Atomic Design metodolojisini tekrar anlatmayacağım; onu detaylıca işlediğim ayrı bir yazı var.

Ama aralarındaki farkı net koymak gerekiyor:

Atomic Design, “bunu nereye koyayım?” sorusunu cevaplıyor. Atom, molecule, organism gibi katmanlarla isimlendirme ve klasörleme dili veriyor. Mimari bir metodoloji.

CDD ise “bunu nasıl geliştireyim?” sorusunu cevaplıyor. İzolasyon, story yazımı, test ve dokümantasyon disiplinini getiriyor. Bir geliştirme yaklaşımı.

Kesiştikleri yer ise şu: ikisi de arayüzü küçük parçalardan kuruyor ve ikisi de Storybook ile çok iyi çalışıyor. Atomic Design iskeleti veriyor, CDD o iskeletin içini doldurma alışkanlığını kazandırıyor. Birlikte kullanıldığında proje hem düzenli klasörleniyor hem de sürdürülebilir şekilde büyüyor.

Atomic Design: Nereye koyuyoruz? (Mimari)

CDD: Nasıl geliştiriyoruz? (Yaklaşım)

Kesişim: Küçük parçalar + Storybook + izolasyon

🗺️ Ne Zaman CDD’ye geçmeli

★ UI karmaşıksa ve ekran sayısı artacaksa

★ Birden fazla geliştirici UI’a dokunuyorsa

★ Tasarım tutarlılığı kritikse

★ Test ve kalite sürecini component seviyesine taşımak istiyorsanız

Şu durumlarda daha hafif ilerlemek mantıklı olabilir:

★ Proje çok küçük ve kısa ömürlüyse

★ UI çok az tekrar ediyorsa

★ İlk fazda hız her şeyin önündeyse

📬 Geri Bildirim

Makaleyi yazarken, kaynakları belirleme, araştırma ve yazı denetimi için Grok 4.20 beta Reasoning, Claude Sonnet 4.6 modellerini kullandım. Resimleri üretmek için ise Gemini 3 Pro Preview 2k (Nano Banana Pro) modelini kullandım.

Yazı ile ilgili tavsiye, öneri, eleştirileri dikkate alıyorum. İletişime geçmek isterseniz bana websitemdeki sosyal medya adreslerimden veya Linkedin üzerinden ulaşabilirsiniz.

Sevgiyle kalın, Yasin 🤗

📚 Makaleyi Yazarken Kullandığım Kaynaklar