03 / 09Project
Proje Yapısını Düzenleme
Dağınık bir e-ticaret projesini feature-based yapıya taşıma.
Senaryo
Elinizde küçük bir mağaza var. Ürünler listeleniyor, sepete ekleniyor, detay sayfası açılıyor. Uygulama çalışıyor ama yapısı dağınık:
| Sorun | Nerede |
|---|---|
| Dosyalar türe göre dizilmiş | components/, hooks/, utils/, types/ |
| Genel dosya adları | utils/utils.ts, types/types.ts |
PascalCase dosya adları | ProductCard.tsx, useCart.ts |
../../../ import'ları | app/products/[slug]/page.tsx |
export * barrel'ı ve döngü | components/index.ts ile hooks/useCart.ts |
| Ürün kartı sepeti biliyor | ProductCard içinde AddToCartButton |
Göreviniz bu projeyi Klasör Yapısı ve İsimlendirme ile Component ve Kod Organizasyonu derslerindeki yapıya taşımak. Tek kural var: kullanıcı hiçbir farkı görmemeli.
Başlangıç ve çözüm GitHub'da: examples/architecture/restructure-project. starter/ klasöründen başlayın, takıldığınızda solution/ ile karşılaştırın.
Tip
Proje next@16.4.2, eslint@9.39.5 ve @playwright/test@1.64.1 ile yazıldı. Testler Chromium'da production build üzerinde çalıştırıldı.
Adım 1: Önce Testi Çalıştırın
Martin Fowler'ın tanımıyla refactoring, kodun iç yapısını gözlemlenebilir davranışını değiştirmeden düzenlemektir.1 Davranışın değişmediğini bilmenin yolu bir testtir.
Projede bir Playwright testi hazır. Ürünleri sepete ekliyor ve başlıktaki özeti kontrol ediyor:
// e2e/cart.spec.ts
await page.getByRole("button", { name: "Mekanik Klavye sepete ekle" }).click();
await page.getByRole("button", { name: "Kablosuz Mouse sepete ekle" }).click();
await expect(summary).toHaveText("Sepet: 2 ürün, ₺3.398,00");$ cd starter
npm install
npx playwright install chromium
npm run test:e2eİki test geçmeli. Test dosya yollarını, bileşen adlarını ya da state'i bilmiyor; yalnızca kullanıcının gördüğüne bakıyor. Kent C. Dodds, iç ayrıntıya bağlı testlerin refactoring sırasında kod sağlamken kırıldığını yazar.2 Bu test ise taşıma boyunca hiç değişmeyecek.
Adım 2: Kuralları Ekleyin
Kuralları en başta açın. Lint hataları yapılacak işlerin listesi olur.
// eslint.config.mjs
{
files: ["src/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": ["error", {
patterns: [{ regex: "^\\.\\./", message: "Use the `@/` alias instead of `../`." }],
}],
"import/no-cycle": "error",
"import/no-restricted-paths": ["error", {
zones: [
{ target: "./src/features/cart", from: "./src/features", except: ["./cart"] },
{ target: "./src/features/products", from: "./src/features", except: ["./products"] },
{ target: "./src/features", from: "./src/app" },
{ target: ["./src/components", "./src/helpers"], from: ["./src/features", "./src/app"] },
],
}],
},
},import eklentisini eslint-config-next zaten kaydediyor, ayrıca kurmanız gerekmez.
no-restricted-importsyalnızca../ile başlayan import'ları yasaklar.3import/no-cycledöngüsel import'u bulur.4import/no-restricted-pathsözelliklerin birbirini import etmesini ve akışın tersine dönmesini engeller.5
Warning
../ yasağı için import/no-relative-parent-imports kuralını kullanmayın. Kural çözümlenen dosya yoluna bakıyor. Denemede @/components/ui/button gibi alias'lı import'ları da hata saydı.
$ pnpm lintsrc/components/CartSummary.tsx
3:1 error '../hooks/useCart' import is restricted from being used by a pattern. Use the `@/` alias instead of `../` no-restricted-imports
3:1 error Dependency cycle via ../components:3 import/no-cycle
✖ 22 problems (22 errors, 0 warnings)Zone hataları henüz çıkmıyor, çünkü features/ klasörü yok. Onlar taşıma sırasında görünecek.
Adım 3: Ortak Kodu Ayırın
utils/utils.ts üç farklı işi bir arada tutuyor. Her fonksiyonu kullanıldığı yere taşıyın:
| Fonksiyon | Yeni yeri | Neden |
|---|---|---|
formatPrice | helpers/format-price.ts | Hem ürün hem sepet kullanıyor |
getCartTotal | features/cart/helpers/cart-total.ts | Yalnızca sepet kullanıyor |
getProductBySlug | features/products/data/products.ts | Ürün verisiyle birlikte durmalı |
Button de iki özelliğin ortak parçası; components/ui/button.tsx'e gider. types/types.ts ikiye bölünür: Product ürüne, CartItem sepete.
Ortak katmana yalnızca iki özelliğin gerçekten paylaştığı kod çıkar.6 Gerisi özelliğin içinde kalır; Kent C. Dodds'ın dediği gibi kod, ilgili olduğu yere yakın durur.7
Adım 4: Özellikleri Taşıyın
src/
├── app/
├── components/ui/button.tsx
├── helpers/format-price.ts
└── features/
├── products/
│ ├── components/product-card.tsx
│ ├── data/products.ts
│ ├── types/product.ts
│ └── index.ts
└── cart/
├── components/{cart-provider,cart-summary,add-to-cart-button}.tsx
├── helpers/cart-total.ts
├── types/cart-item.ts
└── index.tsDosya adlarını kebab-case yapın. Linux'ta çalışan CI büyük/küçük harfi ayırır; tek bir yazım seçmek canlıda "Module not found" sürprizini önler.8 Import'ları @/ ile yazın; create-next-app bu alias'ı kendisi kurar.9
Adım 5: Döngüyü Kırın
Başlangıçta döngü şöyle kuruluyor: CartSummary → hooks/useCart → components/index.ts → CartSummary. export * barrel'ı her şeyi her şeye bağlıyor.
İki değişiklik yeter:
useCart'ıcart-provider.tsx'e taşıyın. Context ve hook aynı dosyada durur.Özelliğin kökündeki
index.ts'i ince tutun: yalnızca dışarının kullandığı parçalar, adlandırılmış re-export ile.10
// features/cart/index.ts
export { AddToCartButton } from "./components/add-to-cart-button";
export { CartProvider } from "./components/cart-provider";
export { CartSummary } from "./components/cart-summary";
export type { CartItem } from "./types/cart-item";Özelliğin kendi dosyaları birbirini index.ts üzerinden değil, doğrudan dosya yoluyla import eder. TkDodo, barrel'ların döngüsel import'a ne kadar kolay yol açtığını anlatır.11
Adım 6: Özellikleri Ayırın
ProductCard dosyasını features/products'a taşıdığınızda lint şunu söyler:
src/features/products/components/product-card.tsx
2:33 error Unexpected path "@/features/cart" imported in restricted zone import/no-restricted-pathsÜrün kartı sepeti bilmemeli. Butonu kartın dışından, bir prop ile verin:
// features/products/components/product-card.tsx
export function ProductCard({ product, action }: { product: Product; action?: React.ReactNode }) {
return (
<article className="card">
<h2><Link href={`/products/${product.slug}`}>{product.name}</Link></h2>
<p>{formatPrice(product.price)}</p>
{action}
</article>
);
}// app/page.tsx
<ProductCard product={product} action={<AddToCartButton {...product} />} />İki özelliği birleştirmek sayfanın işi. Akış tek yönlü kalır: ortak kod → özellikler → uygulama.6
Adım 7: Son Kontrol
$ pnpm lint && npm run typecheck && npm run test:e2eÜçü de hatasız geçmeli. Test dosyası tek satır değişmeden geçiyorsa taşıma davranışı korumuş demektir.
Tip
Bu işi bir kodlama ajanına verecekseniz önce Adım 1 ve 2'yi siz yapın. Test ve lint, ajanın işini kendi kontrol edebileceği bir hedef olur.
Kendin Dene
features/productsiçinden@/features/cart/components/cart-providerimport edin ve hatayı görün.Bir
checkoutözelliği ekleyin. Zone listesine kendi satırını yazın; sepet toplamına ihtiyaç duyarsacartTotal'ı nereye taşımanız gerektiğine karar verin.features/cart/index.ts'iexport *ile yazın, sonrauseCart'ı oradan import edipimport/no-cycleçıktısına bakın.
Kaynaklar
Refactoring'in tanımı: gözlemlenebilir davranışı değiştirmeden iç yapıyı düzenlemek. 1 Eylül 2004.
İç ayrıntıya bağlı testlerin refactoring'de yanlış alarm vermesi. 17 Ağustos 2020.
`patterns` içinde `regex` ile import yolu kısıtlama.
Döngüsel import tespiti ve `maxDepth`.
`target`, `from` ve `except` ile klasörler arası import yasağı.
Feature klasörleri, özellikler arası import yasağı ve tek yönlü akış.
Kodu ilgili olduğu yere yakın koyma ilkesi. 17 Haziran 2019.
Büyük/küçük harf duyarlılığı yüzünden canlıda kırılan import'lar.
`@/*` alias'ı ve mutlak import'lar.
Özelliğin dışa açık sözleşmesi olarak `index.ts` ve `export *` sorunu.
Barrel dosyalarının döngüsel import'a ve yavaşlığa etkisi. 26 Temmuz 2024.
Last updated: October 22, 2026
