728x90

Laravel hasManyThrough 관계 완벽 이해하기

🎯 들어가며

Laravel의 Eloquent 관계 중에서 hasManyThrough는 초보 개발자들이 가장 헷갈려하는 관계 중 하나입니다. 하지만 일단 이해하고 나면 복잡한 데이터베이스 쿼리를 놀라울 정도로 간단하게 만들어주는 강력한 도구라는 것을 알게 될 거예요.

오늘은 국가-주/도-도시 시스템을 예제로 들어 hasManyThrough 관계를 차근차근 분해해보겠습니다.

📊 데이터베이스 구조 이해하기

먼저 우리가 다룰 테이블 구조를 살펴보겠습니다:

countries (국가)
├── id
├── name
├── code
└── created_at

states (주/도)
├── id
├── country_id (countries.id 참조)
├── name
├── code
└── created_at

cities (도시)
├── id
├── state_id (states.id 참조)
├── name
├── population
└── created_at

🏗️ 관계도 시각화

Country (국가)
    │
    │ hasMany
    ▼
State (주/도)
    │
    │ hasMany
    ▼
City (도시)

실제 데이터 예시:

  • Country #1: "미국"
    • State #1: "캘리포니아"
      • City #1: "로스앤젤레스"
      • City #2: "샌프란시스코"
    • State #2: "텍사스"
      • City #3: "휴스턴"
      • City #4: "댈러스"

🤔 문제 상황: 특정 국가의 모든 도시를 가져오고 싶다면?

일반적인 접근 방법으로는 이렇게 할 수 있습니다:

// 방법 1: 중첩 반복문 (비효율적)
$country = Country::find(1); // 미국
$allCities = [];

foreach ($country->states as $state) {
    foreach ($state->cities as $city) {
        $allCities[] = $city;
    }
}

하지만 이 방법은 N+1 쿼리 문제를 일으킬 수 있고, 코드도 복잡해집니다.

✨ hasManyThrough의 마법

hasManyThrough를 사용하면 중간 모델을 "통과해서" 최종 모델에 직접 접근할 수 있습니다:

class Country extends Model 
{
    // 직접적인 관계: Country → State
    public function states() 
    {
        return $this->hasMany(State::class);
    }

    // 간접적인 관계: Country → State → City
    public function cities() 
    {
        return $this->hasManyThrough(
            City::class,             // 🎯 최종 목표 모델
            State::class,            // 🔗 중간 연결 모델
            'country_id',            // 📌 중간모델의 외래키 (State.country_id)
            'state_id',              // 📌 최종모델의 외래키 (City.state_id)
            'id',                    // 🔑 시작모델의 로컬키 (Country.id)
            'id'                     // 🔑 중간모델의 로컬키 (State.id)
        );
    }
}

📝 매개변수 설명

순서 매개변수 설명 예시 값
1 최종 모델 최종적으로 가져오고 싶은 모델 City::class
2 중간 모델 거쳐가야 하는 중간 모델 State::class
3 중간모델 외래키 중간 테이블에서 현재 모델을 참조하는 키 'country_id'
4 최종모델 외래키 최종 테이블에서 중간 모델을 참조하는 키 'state_id'
5 현재모델 로컬키 현재 모델의 기본키 'id'
6 중간모델 로컬키 중간 모델의 기본키 'id'

💻 실제 사용 예시

// 이제 미국의 모든 도시를 한 번에 가져올 수 있습니다!
$usa = Country::where('code', 'US')->first();
$allUsCities = $usa->cities; // 🎉 간단하죠?

// 특정 조건을 추가할 수도 있습니다
$largeCities = $usa->cities()
    ->where('population', '>', 1000000)
    ->orderBy('population', 'desc')
    ->get();

// 개수도 쉽게 셀 수 있습니다
$cityCount = $usa->cities()->count();
echo "미국에는 총 {$cityCount}개의 도시가 있습니다.";

🔍 생성되는 SQL 쿼리 분석

Laravel이 hasManyThrough로 어떤 SQL을 생성하는지 살펴보겠습니다:

SELECT 
    cities.*, 
    states.country_id as laravel_through_key
FROM cities 
INNER JOIN states 
    ON states.id = cities.state_id 
WHERE states.country_id = 1

💡 : DB::enableQueryLog()를 사용하면 실제 실행되는 쿼리를 확인할 수 있습니다!

🛠️ 실무에서 활용하기

1. 이커머스 배송 시스템

// 특정 국가로 배송 가능한 모든 도시 조회
$korea = Country::where('code', 'KR')->first();
$deliverableCities = $korea->cities()
    ->where('is_delivery_available', true)
    ->orderBy('name')
    ->get();

// 배송비 계산을 위한 도시별 인구 정보
$populatedCities = $korea->cities()
    ->where('population', '>', 100000)
    ->select('name', 'population')
    ->get();

2. 집계 함수 활용

// 국가별 총 도시 개수
$countriesWithCityCounts = Country::withCount('cities')->get();

foreach ($countriesWithCityCounts as $country) {
    echo "{$country->name}: {$country->cities_count}개의 도시\n";
}

// 가장 많은 도시를 가진 국가 찾기
$countryWithMostCities = Country::withCount('cities')
    ->orderBy('cities_count', 'desc')
    ->first();

3. 지역별 통계 분석

// 국가별 평균 도시 인구
$avgPopulationByCountry = Country::join('states', 'countries.id', '=', 'states.country_id')
    ->join('cities', 'states.id', '=', 'cities.state_id')
    ->groupBy('countries.id', 'countries.name')
    ->selectRaw('countries.name, AVG(cities.population) as avg_population')
    ->get();

4. Eager Loading으로 성능 최적화

// N+1 쿼리 문제 방지
$countries = Country::with(['states.cities', 'cities'])->get();

// 특정 조건이 있는 관계도 미리 로드
$countries = Country::with([
    'cities' => function ($query) {
        $query->where('population', '>', 500000);
    }
])->get();

🔧 Laravel Tinker로 테스트하기

실제로 동작하는지 확인해보고 싶다면 Tinker를 사용해보세요:

php artisan tinker
// Tinker에서 실행
$usa = App\Models\Country::where('code', 'US')->first();

// 전통적인 방법
$usa->states->flatMap->cities;

// hasManyThrough 방법
$usa->cities;

// 두 결과가 같은지 확인
$usa->states->flatMap->cities->pluck('id')->sort()->values() === 
$usa->cities->pluck('id')->sort()->values();

// 쿼리 로그 확인
DB::enableQueryLog();
$usa->cities;
dd(DB::getQueryLog());

⚠️ 주의사항과 제한사항

1. 중간 모델의 속성 접근 불가

// ❌ 이렇게 할 수 없습니다
foreach ($usa->cities as $city) {
    echo $city->state->name; // 중간 모델 정보 없음
}

// ✅ 대신 이렇게 해야 합니다
$usa->load('cities.state');
foreach ($usa->cities as $city) {
    echo $city->state->name;
}

2. 복잡한 조건 처리

// 중간 모델의 조건을 추가하고 싶을 때
$usa->cities()
    ->whereHas('state', function ($query) {
        $query->where('timezone', 'PST');
    })
    ->get();

// 또는 직접 조인 조건 추가
$usa->cities()
    ->join('states', 'cities.state_id', '=', 'states.id')
    ->where('states.timezone', 'PST')
    ->select('cities.*')
    ->get();

🌟 고급 활용 패턴

1. 동적 관계 구성

class Country extends Model 
{
    public function citiesByPopulation($minPopulation = 0)
    {
        return $this->hasManyThrough(City::class, State::class)
            ->where('population', '>=', $minPopulation);
    }

    public function majorCities() 
    {
        return $this->citiesByPopulation(1000000);
    }
}

2. 중간 테이블 정보 포함

// 중간 테이블 정보도 함께 가져오기
$usa->cities()
    ->join('states', 'cities.state_id', '=', 'states.id')
    ->select(
        'cities.*', 
        'states.name as state_name',
        'states.code as state_code'
    )
    ->get();

📚 다른 관계들과의 비교

관계 타입 사용 시기 예시
hasMany 직접적인 1:N 관계 Country → States
hasManyThrough 간접적인 1:N 관계 Country → Cities (via States)
belongsToMany N:M 관계 User ↔ Roles
hasOneThrough 간접적인 1:1 관계 Country → Capital (via States)

🌍 실제 사용 사례

1. 글로벌 서비스 지역 설정

// 사용자가 선택한 국가의 모든 배송 가능 도시
$selectedCountry = auth()->user()->country;
$availableCities = $selectedCountry->cities()
    ->where('shipping_available', true)
    ->orderBy('name')
    ->pluck('name', 'id');

2. 관리자 대시보드 통계

// 국가별 서비스 현황
$serviceStats = Country::withCount([
    'cities',
    'cities as active_cities_count' => function ($query) {
        $query->where('service_active', true);
    }
])->get();

3. API 응답 최적화

// JSON API에서 중첩 데이터 효율적으로 반환
return Country::with('cities:id,state_id,name,population')
    ->find($countryId)
    ->cities
    ->groupBy('state_id')
    ->map(function ($cities) {
        return $cities->sortByDesc('population')->values();
    });

🎉 정리하며

hasManyThrough는 복잡해 보이지만, 한 번 이해하고 나면 데이터베이스 관계를 매우 우아하게 표현할 수 있는 도구입니다.

핵심 포인트 요약:

  1. 목적: 중간 모델을 거쳐 최종 모델에 접근
  2. 구조: 시작 모델 → 중간 모델 → 최종 모델
  3. 장점: 복잡한 쿼리를 간단한 관계로 표현
  4. 주의: 중간 모델의 속성에는 직접 접근할 수 없음

실무 활용 팁:

  • 항상 실제 SQL을 확인하여 성능을 검토하세요
  • 복잡한 조건이 필요하다면 whereHas나 별도 쿼리를 고려하세요
  • Eager Loading을 활용하여 N+1 문제를 방지하세요
  • 대용량 데이터에서는 페이지네이션을 함께 사용하세요

이런 지리적 관계는 특히 이커머스, 배송 서비스, 지역 기반 서비스에서 매우 유용합니다.

728x90
728x90

Next.js RSC(React Server Components)란? 초보자도 쉽게 이해하는 가이드

웹 개발을 하다 보면 "페이지가 빨리 로드되었으면 좋겠다"는 생각을 자주 하게 됩니다. Next.js의 RSC(React Server Components)는 바로 이런 고민을 해결해주는 새로운 기술입니다.

🤔 RSC가 뭐길래?

RSC를 한 줄로 설명하면 "서버에서 미리 만들어서 보내주는 React 컴포넌트"입니다.

기존에는 이런 식으로 작동했어요:

  1. 사용자가 웹사이트 접속
  2. 빈 HTML + JavaScript 파일 다운로드
  3. JavaScript가 실행되면서 화면을 그림

RSC는 이렇게 바뀝니다:

  1. 사용자가 웹사이트 접속
  2. 서버에서 이미 완성된 HTML을 받음
  3. 훨씬 빠르게 화면이 나타남!

🏠 집 짓기로 비유해보자

기존 방식(CSR)은 건축 자재만 배송받고 현장에서 집을 짓는 것 같아요. 시간이 오래 걸리죠.

RSC는 공장에서 미리 조립한 프리팹 하우스를 배송받는 것과 비슷합니다. 현장에 도착하자마자 바로 살 수 있어요!

📊 기존 방식과 뭐가 다른가요?

전통적인 CSR (Client-Side Rendering)

// 브라우저에서 실행
function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 페이지 로드 후 API 호출
    fetch('/api/user').then(setUser);
  }, []);

  if (!user) return <div>로딩중...</div>;
  return <div>안녕하세요, {user.name}님!</div>;
}

문제점: 페이지 → JavaScript 로드 → API 호출 → 화면 그리기 (느려요! 😵‍💫)

Next.js RSC 방식

// 서버에서 실행
async function UserProfile() {
  // 서버에서 미리 데이터 가져오기
  const user = await fetch('https://api.example.com/user').then(r => r.json());

  // 완성된 HTML을 클라이언트에 전송
  return <div>안녕하세요, {user.name}님!</div>;
}

장점: 사용자가 접속하자마자 완성된 화면을 볼 수 있어요! ⚡

🎯 RSC의 핵심 장점

1. 빠른 초기 로딩

  • 서버에서 미리 HTML을 만들어서 보내주니까 화면이 빨리 나타나요
  • 사용자는 "로딩중..." 화면을 덜 보게 됩니다

2. SEO 친화적

  • 검색엔진이 페이지 내용을 쉽게 읽을 수 있어요
  • 완성된 HTML이 있으니까 크롤링이 수월합니다

3. 더 작은 JavaScript 번들

  • 서버 컴포넌트는 클라이언트로 전송되지 않아요
  • 브라우저에서 다운로드할 파일이 줄어듭니다

💻 실제로 어떻게 사용하나요?

Next.js 13+ 버전에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트입니다.

서버 컴포넌트 예시

// app/page.js - 기본적으로 서버 컴포넌트
async function HomePage() {
  // 서버에서 데이터 가져오기
  const posts = await fetch('https://api.blog.com/posts').then(r => r.json());

  return (
    <div>
      <h1>블로그 포스트</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
        </article>
      ))}
    </div>
  );
}

export default HomePage;

클라이언트 컴포넌트가 필요한 경우

'use client' // 이 지시어로 클라이언트 컴포넌트 만들기

import { useState } from 'react';

function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button onClick={() => setLikes(likes + 1)}>
      👍 {likes}
    </button>
  );
}

🤷‍♂️ 언제 뭘 써야 하나요?

서버 컴포넌트를 쓰세요:

  • 데이터를 가져와서 보여주기만 하는 경우
  • 블로그 글, 상품 목록, 사용자 프로필 등
  • SEO가 중요한 페이지

클라이언트 컴포넌트를 쓰세요:

  • 사용자와 상호작용이 필요한 경우
  • 버튼 클릭, 폼 입력, 애니메이션 등
  • useState, useEffect 같은 React Hook을 사용할 때

🔄 둘이 함께 사용하기

// 서버 컴포넌트
async function ProductPage({ id }) {
  const product = await fetch(`/api/products/${id}`).then(r => r.json());

  return (
    <div>
      {/* 서버에서 렌더링된 정적 내용 */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <img src={product.image} alt={product.name} />

      {/* 인터랙티브한 클라이언트 컴포넌트 */}
      <AddToCartButton productId={id} />
      <ReviewForm productId={id} />
    </div>
  );
}

🎉 정리하면

RSC는 웹사이트를 더 빠르고 효율적으로 만들어주는 Next.js의 신기술입니다.

핵심 포인트:

  • 서버에서 미리 HTML을 만들어서 보내줘요
  • 초기 로딩이 빨라지고 SEO에 좋아요
  • 상호작용이 필요한 부분만 클라이언트 컴포넌트로 만들면 돼요
  • Next.js 13+에서는 기본값이라 특별히 설정할 것도 없어요

처음에는 "어떤 걸 서버에서, 어떤 걸 클라이언트에서 처리할지" 고민될 수 있지만, 실제로 써보면 생각보다 직관적이에요. 데이터만 보여주면 서버, 클릭이나 입력이 있으면 클라이언트라고 생각하시면 됩니다!

🔗 함께 보면 좋은 링크

728x90
728x90

Next.js Link의 prefetch={false}: 언제 그리고 왜 사용해야 할까?

Next.js의 <Link> 컴포넌트는 기본적으로 링크된 페이지를 미리 가져오는 프리패치(prefetch) 기능을 제공합니다. 하지만 때로는 이 기능을 비활성화해야 하는 경우가 있습니다. 마치 여행 전에 모든 짐을 미리 싸두는 것이 때로는 불필요할 수 있는 것처럼 말이죠.

기본 개념: Next.js의 프리패치란?

Next.js에서 <Link> 컴포넌트는 기본적으로 prefetch={true}로 설정되어 있습니다. 이는 사용자가 링크를 클릭하기 전에 해당 페이지의 JavaScript 번들을 미리 다운로드한다는 의미입니다.

// 기본 동작 - 프리패치가 활성화됨
<Link href="/about">
  소개 페이지로 이동
</Link>

// 명시적으로 프리패치 비활성화
<Link href="/dashboard" prefetch={false}>
  대시보드로 이동
</Link>

기본 프리패치는 뷰포트에 링크가 나타날 때 자동으로 트리거됩니다. 이로 인해 사용자가 실제로 클릭할 때 페이지가 즉시 로드되어 더 빠른 사용자 경험을 제공합니다.

프리패치를 비활성화해야 하는 경우

1. 거의 사용되지 않는 링크

모든 사용자가 클릭하지 않을 링크들은 불필요한 네트워크 트래픽을 발생시킵니다.

function Footer() {
  return (
    <footer>
      <Link href="/privacy-policy" prefetch={false}>
        개인정보처리방침
      </Link>
      <Link href="/terms-of-service" prefetch={false}>
        이용약관
      </Link>
      {/* 대부분의 사용자가 클릭하지 않는 링크들 */}
    </footer>
  );
}

2. 조건부로 표시되는 링크

특정 조건에서만 나타나는 링크는 프리패치할 필요가 없습니다.

function UserProfile({ user, isAdmin }) {
  return (
    <div>
      <h1>{user.name}</h1>
      {isAdmin && (
        <Link href="/admin-panel" prefetch={false}>
          관리자 패널
        </Link>
      )}
    </div>
  );
}

3. 큰 번들 크기를 가진 페이지

대용량 페이지나 많은 의존성을 가진 페이지는 선택적으로 로드하는 것이 좋습니다.

function NavigationMenu() {
  return (
    <nav>
      <Link href="/">홈</Link>
      <Link href="/products">제품</Link>
      {/* 차트 라이브러리 등 큰 번들을 가진 페이지 */}
      <Link href="/analytics" prefetch={false}>
        분석 대시보드
      </Link>
    </nav>
  );
}

성능 측면에서의 고려사항

장점

  • 네트워크 대역폭 절약: 불필요한 리소스 다운로드를 방지합니다
  • 초기 로딩 성능 향상: 현재 페이지의 로딩에 집중할 수 있습니다
  • 모바일 친화적: 제한된 데이터 환경에서 특히 유용합니다

단점

  • 클릭 후 지연: 사용자가 링크를 클릭할 때 추가 로딩 시간이 발생합니다
  • 사용자 경험 저하: 즉시 페이지 전환을 기대하는 사용자에게는 아쉬울 수 있습니다

개발자 도구로 프리패치 확인하기

브라우저의 개발자 도구 Network 탭에서 프리패치 동작을 확인할 수 있습니다:

// 이 컴포넌트로 테스트해보세요
function TestPrefetch() {
  return (
    <div>
      <Link href="/page1">프리패치 있음</Link>
      <Link href="/page2" prefetch={false}>프리패치 없음</Link>
    </div>
  );
}

Network 탭에서 "page1"에 대한 요청이 링크가 뷰포트에 나타날 때 자동으로 발생하는 것을 확인할 수 있습니다.

실용적인 가이드라인

프리패치를 유지해야 하는 경우

  • 메인 네비게이션 링크
  • 사용자가 자주 방문하는 페이지
  • 작은 번들 크기를 가진 페이지

프리패치를 비활성화해야 하는 경우

  • 푸터의 법적 문서 링크
  • 관리자 전용 페이지
  • 외부 리소스가 많은 페이지
  • 조건부로 표시되는 링크

핵심 정리

prefetch={false}는 성능 최적화를 위한 강력한 도구입니다. 모든 링크에 적용할 필요는 없지만, 사용자 경험과 성능 사이의 균형을 맞추는 데 도움이 됩니다.

기억할 점:

  • 기본값은 prefetch={true}입니다
  • 거의 사용되지 않는 링크에는 prefetch={false}를 사용하세요
  • 개발자 도구로 실제 동작을 확인해보세요
  • Lighthouse를 통해 번들 분석을 진행하세요

더 자세한 정보는 Next.js 공식 문서를 참고하시기 바랍니다.

728x90
728x90

프론트엔드 개발자를 위한 4px 시스템: 일관된 디자인의 비밀

들어가며

프론트엔드 개발을 하다 보면 "왜 이 요소들이 시각적으로 정렬되지 않을까?", "디자이너와의 협업에서 왜 자꾸 픽셀 단위로 미세 조정을 요구할까?"라는 의문을 가져본 적이 있을 것입니다. 이런 문제들의 해답 중 하나가 바로 4px 시스템(4px Grid System)에 있습니다.

4px 시스템은 단순히 모든 spacing 값을 4의 배수로 맞추는 것 이상의 의미를 가집니다. 이는 디자인의 일관성, 개발의 효율성, 그리고 사용자 경험의 향상을 동시에 달성할 수 있는 강력한 방법론입니다.

왜 하필 4인가?

수학적 근거

4라는 숫자가 선택된 이유는 여러 수학적, 기술적 근거가 있습니다:

1. 다양한 배수 관계

  • 4는 2의 배수이면서 동시에 8의 약수입니다
  • 1, 2, 4, 8, 16, 32... 이런 배수 체계는 컴퓨터 과학에서 매우 친숙합니다
  • 다양한 크기의 요소들을 자연스럽게 조화시킬 수 있습니다

2. 시각적 인지

  • 인간의 눈은 4px 단위의 차이를 명확하게 구분할 수 있습니다
  • 2px는 너무 미세하고, 8px는 때로 너무 큰 점프가 될 수 있습니다
  • 4px는 시각적으로 의미 있는 최소 단위입니다

3. 디바이스 호환성

  • 대부분의 디스플레이는 픽셀 밀도가 2의 배수 관계를 가집니다
  • Retina 디스플레이, 고해상도 모니터에서도 일관된 렌더링이 가능합니다

4px 시스템의 핵심 이점

1. 디자인 일관성 확보

/* 기존 방식 - 불규칙한 spacing */
.card {
  padding: 15px 18px 20px 16px;
  margin: 12px 0 25px 0;
}

.button {
  padding: 7px 13px;
  margin: 5px 8px;
}

/* 4px 시스템 적용 */
.card {
  padding: 16px 20px 20px 16px; /* 4의 배수로 통일 */
  margin: 12px 0 24px 0;
}

.button {
  padding: 8px 12px;
  margin: 4px 8px;
}

2. 개발 생산성 향상

4px 시스템을 적용하면 디자이너와 개발자 간의 커뮤니케이션이 훨씬 명확해집니다. "조금 더 여백을 주세요"라는 모호한 요청 대신 "4px 증가시켜 주세요" 또는 "16px에서 20px로 변경해 주세요"라는 구체적인 지시가 가능합니다.

3. 반응형 디자인에서의 일관성

/* 반응형에서도 4px 시스템 유지 */
.container {
  padding: 16px; /* 모바일 */
}

@media (min-width: 768px) {
  .container {
    padding: 24px; /* 태블릿 */
  }
}

@media (min-width: 1024px) {
  .container {
    padding: 32px; /* 데스크톱 */
  }
}

실무에서의 구현 방법

1. CSS Custom Properties 활용

:root {
  /* Spacing Scale - 4px 기반 */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;
}

/* 사용 예시 */
.section {
  padding: var(--space-8) var(--space-6);
  margin-bottom: var(--space-12);
}

.card {
  padding: var(--space-4);
  gap: var(--space-3);
}

2. Tailwind CSS와의 조화

Tailwind CSS는 기본적으로 4px 기반의 spacing 시스템을 사용합니다:

<!-- Tailwind의 spacing scale -->
<div class="p-4 m-2 space-y-6">
  <!-- p-4 = 16px, m-2 = 8px, space-y-6 = 24px -->
  <div class="px-6 py-3">
    <!-- px-6 = 24px, py-3 = 12px -->
  </div>
</div>

3. SCSS/Sass 함수 활용

// 4px 기반 spacing 함수
@function space($multiplier) {
  @return $multiplier * 4px;
}

// 사용 예시
.component {
  padding: space(4); // 16px
  margin: space(6) space(3); // 24px 12px
}

// 또는 mixin으로 활용
@mixin spacing($padding: 0, $margin: 0) {
  padding: space($padding);
  margin: space($margin);
}

.button {
  @include spacing($padding: 3, $margin: 2);
  // padding: 12px, margin: 8px
}

타이포그래피에서의 4px 시스템

Font Size 적용

:root {
  /* Typography Scale - 4px 기반 */
  --text-xs: 12px;   /* 3 × 4 */
  --text-sm: 14px;   /* 3.5 × 4 */
  --text-base: 16px; /* 4 × 4 */
  --text-lg: 18px;   /* 4.5 × 4 */
  --text-xl: 20px;   /* 5 × 4 */
  --text-2xl: 24px;  /* 6 × 4 */
  --text-3xl: 30px;  /* 7.5 × 4 */
  --text-4xl: 36px;  /* 9 × 4 */
}

Line Height 최적화

.text-content {
  font-size: var(--text-base); /* 16px */
  line-height: 24px; /* 16px의 1.5배, 4px의 6배 */
}

.heading {
  font-size: var(--text-2xl); /* 24px */
  line-height: 32px; /* 24px의 1.33배, 4px의 8배 */
}

컴포넌트 설계에서의 활용

React 컴포넌트 예시

// Spacing props를 받는 Box 컴포넌트
const Box = ({ 
  p = 0,    // padding
  m = 0,    // margin
  px = 0,   // padding-x
  py = 0,   // padding-y
  mx = 0,   // margin-x
  my = 0,   // margin-y
  children,
  ...props 
}) => {
  const spacing = (value) => `${value * 4}px`;

  const styles = {
    padding: p ? spacing(p) : undefined,
    paddingLeft: px ? spacing(px) : undefined,
    paddingRight: px ? spacing(px) : undefined,
    paddingTop: py ? spacing(py) : undefined,
    paddingBottom: py ? spacing(py) : undefined,
    margin: m ? spacing(m) : undefined,
    marginLeft: mx ? spacing(mx) : undefined,
    marginRight: mx ? spacing(mx) : undefined,
    marginTop: my ? spacing(my) : undefined,
    marginBottom: my ? spacing(my) : undefined,
  };

  return (
    <div style={styles} {...props}>
      {children}
    </div>
  );
};

// 사용 예시
<Box p={4} m={2}>
  <Box px={6} py={3}>
    콘텐츠
  </Box>
</Box>

Styled Components와의 조합

import styled from 'styled-components';

const space = (multiplier) => `${multiplier * 4}px`;

const Card = styled.div`
  padding: ${space(4)};
  margin: ${space(3)} 0;
  border-radius: ${space(2)};
`;

const Button = styled.button`
  padding: ${space(2)} ${space(4)};
  margin: ${space(1)};
  font-size: ${space(4)};
`;

레이아웃에서의 4px 시스템

Grid와 Flexbox

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-4); /* 16px */
  padding: var(--space-6); /* 24px */
}

.flex-container {
  display: flex;
  gap: var(--space-3); /* 12px */
  padding: var(--space-4); /* 16px */
}

.flex-item {
  flex: 1;
  padding: var(--space-2); /* 8px */
}

컨테이너 너비 설정

.container {
  max-width: 1200px; /* 300 × 4 */
  margin: 0 auto;
  padding: 0 var(--space-4);
}

.sidebar {
  width: 240px; /* 60 × 4 */
  padding: var(--space-6);
}

.main-content {
  width: calc(100% - 240px - var(--space-8));
  padding: var(--space-6);
}

도구와 워크플로우

1. 브라우저 개발자 도구 활용

브라우저의 개발자 도구에서 요소를 선택할 때 computed styles를 확인하여 4의 배수로 설정되어 있는지 검증할 수 있습니다.

2. Figma/Sketch 플러그인

디자인 도구에서도 4px 그리드를 설정하여 디자이너와 개발자가 같은 기준을 공유할 수 있습니다:

/* Figma에서 내보낸 스타일을 4px 단위로 조정 */
.figma-export {
  /* Original: padding: 15px 17px 19px 13px */
  padding: 16px 16px 20px 12px; /* 4의 배수로 조정 */
}

3. 린터 규칙 설정

// stylelint 규칙 예시
module.exports = {
  rules: {
    'length-zero-no-unit': true,
    'declaration-property-value-whitelist': {
      '/^(margin|padding|gap|top|right|bottom|left|width|height|font-size)$/': [
        /^0$/,
        /^[0-9]*[48]px$/,  // 4 또는 8로 끝나는 px 값만 허용
        /^var\(--space-[0-9]+\)$/  // CSS 변수 허용
      ]
    }
  }
};

예외 상황과 대처 방법

1. 홀수 픽셀이 필요한 경우

때로는 1px border나 3px outline 등이 필요할 수 있습니다. 이런 경우 핵심 spacing은 4px 시스템을 유지하되, 시각적 요소(border, outline, shadow)는 예외로 둡니다:

.button {
  padding: var(--space-3); /* 12px - 4px 시스템 유지 */
  border: 1px solid #ccc;  /* 1px는 예외 */
  box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* shadow offset은 예외 */
}

2. 디자이너와의 협업에서 발생하는 이슈

디자이너가 4px 시스템에 익숙하지 않은 경우, 개발자가 능동적으로 소통하며 4px 단위로 조정된 대안을 제시해야 합니다:

/* 디자이너 요청: padding: 15px */
/* 개발자 제안: padding: 16px (가까운 4의 배수) */

/* 디자이너 요청: margin: 18px */
/* 개발자 제안: margin: 16px 또는 20px 중 선택 */

성능과 유지보수 관점

1. CSS 번들 크기 최적화

4px 시스템을 사용하면 CSS 클래스의 재사용성이 높아져 전체 번들 크기가 줄어들 수 있습니다:

/* Before: 각기 다른 spacing으로 인한 많은 유니크 클래스 */
.component-a { padding: 13px 17px; }
.component-b { padding: 15px 19px; }
.component-c { padding: 11px 21px; }

/* After: 표준화된 spacing으로 클래스 재사용 */
.p-3 { padding: 12px; }
.p-4 { padding: 16px; }
.p-5 { padding: 20px; }

2. 런타임 계산 최소화

JavaScript에서 동적으로 스타일을 계산할 때도 4px 시스템을 활용하면 예측 가능한 결과를 얻을 수 있습니다:

// 4px 기반 spacing 계산 함수
const getSpacing = (multiplier) => `${multiplier * 4}px`;

// 컴포넌트 렌더링에서 활용
const DynamicComponent = ({ level = 1 }) => {
  const padding = getSpacing(level + 2); // 최소 12px부터 시작

  return (
    <div style={{ padding }}>
      레벨 {level} 컴포넌트
    </div>
  );
};

접근성과 사용자 경험

1. 터치 타겟 크기

모바일 환경에서 터치 타겟은 최소 44px × 44px이 권장됩니다. 4px 시스템에서는 이를 48px × 48px(12 × 4)로 설정하여 충분한 터치 영역을 확보할 수 있습니다:

.touch-target {
  min-width: 48px;  /* 12 × 4 */
  min-height: 48px; /* 12 × 4 */
  padding: var(--space-2); /* 추가 패딩 */
}

2. 포커스 인디케이터

키보드 접근성을 위한 포커스 아웃라인도 4px 시스템에 맞춰 설계할 수 있습니다:

.focusable:focus {
  outline: 2px solid #0066cc;
  outline-offset: 2px; /* 총 4px 여백 */
}

팀 협업에서의 도입 전략

1. 점진적 도입

기존 프로젝트에 4px 시스템을 도입할 때는 점진적으로 접근하는 것이 좋습니다:

1단계: 새로운 컴포넌트부터 적용

/* 새로운 컴포넌트는 4px 시스템 적용 */
.new-card {
  padding: var(--space-4);
  margin: var(--space-3);
}

2단계: 기존 컴포넌트 리팩토링

/* 기존 컴포넌트를 점진적으로 변경 */
.legacy-card {
  /* padding: 15px; */ /* 기존 값 주석 처리 */
  padding: var(--space-4); /* 16px로 변경 */
}

3단계: 디자인 시스템 통합

/* 전체 디자인 시스템에 4px 시스템 적용 */
:root {
  /* 모든 spacing 값을 4px 기반으로 정의 */
}

2. 팀 교육과 문서화

# 4px 시스템 가이드라인

## 기본 원칙
- 모든 spacing 값은 4의 배수를 사용합니다
- 예외는 border, outline, shadow 등 장식적 요소로 제한합니다
- CSS 변수를 활용하여 일관성을 유지합니다

## 권장 spacing 값
- 4px: 최소 간격
- 8px: 작은 간격 (아이콘과 텍스트 사이 등)
- 12px: 중간 간격 (버튼 내부 패딩 등)
- 16px: 기본 간격 (카드 패딩 등)
- 24px: 큰 간격 (섹션 간 여백 등)
- 32px 이상: 레이아웃 간격

고급 활용 기법

1. 수직 리듬(Vertical Rhythm) 구현

:root {
  --baseline: 24px; /* 6 × 4 */
}

body {
  line-height: var(--baseline);
}

h1, h2, h3, h4, h5, h6, p, ul, ol {
  margin-bottom: var(--baseline);
}

h1 { 
  font-size: 32px; /* 8 × 4 */
  line-height: 40px; /* 10 × 4, 1.25배 */
}

h2 { 
  font-size: 24px; /* 6 × 4 */
  line-height: 32px; /* 8 × 4, 1.33배 */
}

2. 모듈러 스케일과의 조합

:root {
  /* 황금비(1.618)를 활용한 모듈러 스케일 + 4px 정렬 */
  --scale-1: 16px;  /* 기본값 */
  --scale-2: 24px;  /* 16 × 1.5, 4px로 반올림 */
  --scale-3: 40px;  /* 24 × 1.667, 4px로 반올림 */
  --scale-4: 64px;  /* 40 × 1.6, 4px로 반올림 */
}

3. 반응형에서의 비율 유지

.responsive-spacing {
  padding: var(--space-4); /* 16px */
}

@media (min-width: 768px) {
  .responsive-spacing {
    padding: var(--space-6); /* 24px (1.5배) */
  }
}

@media (min-width: 1024px) {
  .responsive-spacing {
    padding: var(--space-8); /* 32px (2배) */
  }
}

결론

4px 시스템은 단순한 규칙이지만 프론트엔드 개발에 혁신적인 변화를 가져올 수 있습니다. 디자인의 일관성, 개발의 효율성, 그리고 팀 협업의 원활함을 동시에 달성할 수 있는 강력한 도구입니다.

핵심은 완벽함보다는 일관성입니다. 모든 픽셀을 4의 배수로 맞출 필요는 없지만, 주요 spacing 요소들을 4px 시스템으로 표준화하는 것만으로도 충분한 효과를 얻을 수 있습니다.

오늘부터 여러분의 프로젝트에 4px 시스템을 도입해보시기 바랍니다. 작은 변화가 만들어내는 큰 차이를 경험하게 될 것입니다. 디자이너와의 소통이 명확해지고, 코드의 일관성이 향상되며, 사용자에게는 더욱 조화로운 인터페이스를 제공할 수 있을 것입니다.

*"일관성은 작은 것들에서 시작된다. 4px씩 쌓아올린 디자인이 사용자에게 전달하는 품질의 차이를 느껴보세요."*

728x90

'프론트엔드' 카테고리의 다른 글

HTTP 422 상태 코드 완벽 가이드  (0) 2025.06.11
728x90

🧠 Ramda 여덟 번째 스텝: 조건을 조합하는 함수들 – R.allPass, R.anyPass, R.both, R.either, R.not

여러 조건을 한꺼번에 체크해야 하는 경우, &&, ||, ! 같은 연산자를 조합하면 코드가 길어지고 읽기 어려워집니다.

Ramda에서는 조건들을 함수처럼 조합할 수 있습니다. 함수형 스타일의 논리 조합 도구들을 활용해
더 선언적이고 재사용 가능한 조건식을 만들 수 있습니다.


🧠 Topic Summary

주요 논리 조합 함수

함수 설명
R.allPass([f1, f2]) 모든 조건이 참이면 true
R.anyPass([f1, f2]) 하나라도 참이면 true
R.both(f1, f2) f1 && f2
R.either(f1, f2) f1
R.not(f) !f

🛠 Usage Examples

예제 1: 모든 조건 만족 – R.allPass

const isValidUser = R.allPass([
  R.propSatisfies(R.lt(R.__, 100), 'age'),       // age < 100
  R.propSatisfies(R.test(/^A/), 'name')          // name이 A로 시작
]);

isValidUser({ name: 'Alice', age: 30 }); // true
isValidUser({ name: 'Bob', age: 30 });   // false

예제 2: 하나라도 만족 –

R.anyPass

const isSpecialName = R.anyPass([
  R.propEq('name', 'Admin'),
  R.propEq('name', 'Root')
]);

isSpecialName({ name: 'Admin' }); // true
isSpecialName({ name: 'User' });  // false

예제 3: 둘 다 만족 –

R.both

const isLongAndUpper = R.both(
  R.pipe(R.length, R.gt(R.__, 5)),
  R.equals(R.toUpper(R.__))
);

isLongAndUpper('HELLO');    // false
isLongAndUpper('FUNCTION'); // true

예제 4: 둘 중 하나 –

R.either

const isNullOrEmpty = R.either(
  R.isNil,
  R.pipe(R.trim, R.isEmpty)
);

isNullOrEmpty(null);       // true
isNullOrEmpty('   ');      // true
isNullOrEmpty('content');  // false

예제 5: 반전 –

R.not

const isNotAdmin = R.pipe(
  R.propEq('role', 'admin'),
  R.not
);

isNotAdmin({ role: 'user' }); // true

⚠️ Common Pitfalls

1. R.both 와 R.either 는 두 개만 조합 가능**

3개 이상 조건일 경우 R.allPass, R.anyPass를 사용하세요.

// ❌ 잘못된 예
R.both(f1, R.both(f2, f3));

// ✅ 올바른 예
R.allPass([f1, f2, f3]);

2. 조건 함수는

Boolean을 반환해야 함

R.allPass, R.anyPass에 들어가는 함수들은 모두 true 또는 false를 반환해야 정상 작동합니다.


3. 함수형 조건은 재사용에 매우 좋지만, 디버깅이 어려울 수 있음

각 조건이 어떤 결과를 냈는지 확인하려면 중간 로깅이 필요할 수 있습니다.

이를 위해 R.tap(console.log) 같은 도구를 함께 사용할 수 있습니다.


🧩 조합 예제: pipe + 조건 필터

const users = [
  { name: 'Admin', age: 50 },
  { name: 'Alice', age: 30 },
  { name: 'Root', age: 120 },
  { name: 'Bob', age: 25 },
];

const isActiveUser = R.allPass([
  R.propSatisfies(R.lt(R.__, 100), 'age'),
  R.pipe(R.prop('name'), R.complement(R.startsWith('A')))
]);

R.filter(isActiveUser, users);
// 결과: [{ name: 'Bob', age: 25 }]

✅ Call to Action

이제 복잡한 조건도 논리 함수로 선언적으로 조합할 수 있습니다.

R.allPass, R.anyPass를 조합하면 가독성 높고 테스트하기 쉬운 조건식을 만들 수 있어요.

실습 아이디어:

  • 입력값이 비어 있지 않고, 숫자일 때만 처리

  • 사용자 권한이 ‘admin’이거나 ‘editor’일 때만 접근 허용

  • 나이 18세 이상이고 이름이 특정 문자열로 시작하는 조건 만들기

728x90
728x90

🔍 Ramda 일곱 번째 스텝: Lens 시스템 – R.lens, R.view, R.set, R.over

복잡한 중첩 객체에서 특정 값을 읽거나 수정할 때, 직접 경로를 따라가면서 작업하면 코드가 복잡해지기 쉽습니다.
Ramda의 Lens 시스템은 데이터의 특정 부분을 가리키는 '렌즈'를 정의하고, 그 렌즈를 통해 값을 읽거나 바꾸는 방식으로 작동합니다.


🧠 Topic Summary

Lens란?

렌즈(Lens)는 특정 데이터 구조 안의 "부분"을 안전하고 선언적으로 읽거나 수정하는 추상화 도구입니다.

Ramda에서 Lens는 R.lens, R.view, R.set, R.over 네 가지 함수로 구성됩니다:

함수 역할
R.lens(getter, setter) 렌즈 생성
R.view(lens, data) 렌즈로 보기 (읽기)
R.set(lens, value, data) 렌즈로 값 설정
R.over(lens, transformFn, data) 렌즈를 통해 값 변경

🛠 Usage Examples

기본 사용: 이름 필드 조작

import * as R from 'ramda';

const person = { name: 'Alice', age: 30 };

const nameLens = R.lens(R.prop('name'), R.assoc('name'));

// 보기
R.view(nameLens, person); // 'Alice'

// 설정
R.set(nameLens, 'Bob', person); 
// { name: 'Bob', age: 30 }

// 변형
R.over(nameLens, R.toUpper, person); 
// { name: 'ALICE', age: 30 }

중첩 객체: 주소 안의 도시만 조작

const user = {
  profile: {
    name: 'Alice',
    address: {
      city: 'Seoul',
      zip: '12345'
    }
  }
};

const cityLens = R.lensPath(['profile', 'address', 'city']);

R.view(cityLens, user); // 'Seoul'

R.set(cityLens, 'Busan', user);
// 중첩 구조는 유지한 채로 city만 변경

R.over(cityLens, R.toUpper, user); 
// city: 'SEOUL'

🔁 Lens vs Path vs Assoc

기능 접근 방식 사용 예
R.path 읽기 R.path(['a', 'b'], obj)
R.assocPath 쓰기 R.assocPath(['a', 'b'], val, obj)
R.lensPath 읽기/쓰기/변형 모두 가능 R.over(R.lensPath(['a', 'b']), fn, obj)

⚠️ Common Pitfalls

1.

R.lens

는 getter와 setter 함수가 필요함

R.lens(R.prop('key'), R.assoc('key'))처럼 명시적으로 작성해야 합니다.

대부분의 경우는 R.lensProp, R.lensPath를 사용하는 게 간결합니다.

R.lensProp('name'); // name 필드를 위한 렌즈
R.lensPath(['user', 'profile']); // 중첩 구조용 렌즈

2. 렌즈 연산은

불변성을 유지하므로, 원본 객체는 절대 수정되지 않음

const newObj = R.set(lens, val, oldObj);
console.log(oldObj); // 그대로
console.log(newObj); // 변경된 복사본

🧩 조합 예제: pipe와 함께 Lens 활용

const person = {
  name: 'jane',
  score: 85,
  meta: {
    approved: false
  }
};

const scoreLens = R.lensProp('score');
const approvedLens = R.lensPath(['meta', 'approved']);

const processPerson = R.pipe(
  R.over(scoreLens, R.add(5)),
  R.set(approvedLens, true)
);

processPerson(person);
// { name: 'jane', score: 90, meta: { approved: true } }

✅ Call to Action

Lens는 중첩된 데이터를 불변적으로 다루기 위한 함수형 접근 방식의 정수입니다.

이제 여러분도 view, set, over를 조합해 안전하고 예측 가능한 상태 조작 코드를 작성해보세요!

실습 아이디어:

  • 중첩된 유저 상태 객체에서 알림 설정값을 꺼내거나 바꾸기

  • form 데이터에서 특정 필드만 수정하기

  • 배열 내부 객체의 특정 필드 변환하기 (map + lens)

728x90
728x90

🔀 Ramda 여섯 번째 스텝: 조건 분기 – R.ifElse, R.when, R.unless, R.cond

함수형 프로그래밍에서는 if, else, switch 같은 제어문을 덜 사용하고,
조건 자체를 함수처럼 조합하여 흐름을 제어합니다.

Ramda는 이를 위한 여러 함수들을 제공합니다:

  • R.ifElse: 조건 분기 (if/else)
  • R.when: 조건이 참일 때만 함수 실행
  • R.unless: 조건이 거짓일 때만 함수 실행
  • R.cond: 여러 조건을 나열 (switch 또는 else-if)

🧠 Topic Summary

전통적 제어문 vs 함수형 조건

전통적 방식 함수형 방식
if (x > 10) {...} R.ifElse(predicate, ifFn, elseFn)
x > 10 && do() R.when(predicate, fn)
x <= 10 && do() R.unless(predicate, fn)
switch-case R.cond([...])

✅ R.ifElse

R.ifElse(predicateFn, thenFn, elseFn)

예제: 점수가 60 이상이면 통과, 아니면 재시험

const getResult = R.ifElse(
  R.gte(R.__, 60),           // 점수가 60 이상?
  R.always('Pass'),          // 참이면
  R.always('Retake')         // 거짓이면
);

getResult(70); // 'Pass'
getResult(50); // 'Retake'

✅ R.when

R.when(predicateFn, fn)

조건이 참일 때만 fn을 실행하고, 거짓이면 그대로 반환합니다.

예제: 100보다 작으면 2배

const doubleIfSmall = R.when(
  R.lt(R.__, 100),
  R.multiply(2)
);

doubleIfSmall(80); // 160
doubleIfSmall(150); // 150 (그대로 반환)

✅ R.unless

R.unless(predicateFn, fn)

조건이 거짓일 때만 fn을 실행하고, 참이면 그대로 반환합니다.

예제: 100 이상이면 그대로, 아니면 100으로 바꾸기

const ensureAtLeast100 = R.unless(
  R.gte(R.__, 100),
  R.always(100)
);

ensureAtLeast100(120); // 120
ensureAtLeast100(80); // 100

✅ R.cond

R.cond([
  [predicate1, resultFn1],
  [predicate2, resultFn2],
  ...
  [R.T, defaultFn]
])

조건들을 배열로 나열해 switch-case처럼 작동하는 함수입니다.

예제: 점수 등급 출력하기

const grade = R.cond([
  [R.gte(R.__, 90), R.always('A')],
  [R.gte(R.__, 80), R.always('B')],
  [R.gte(R.__, 70), R.always('C')],
  [R.gte(R.__, 60), R.always('D')],
  [R.T, R.always('F')]
]);

grade(85); // 'B'
grade(72); // 'C'
grade(45); // 'F'

⚠️ Common Pitfalls

1. R.ifElse는 반드시 세 개의 함수가 필요하다

R.ifElse(predicate); // ❌ 에러

함수를 꼭 세 개 넣어야 작동합니다: 조건, 참일 때 함수, 거짓일 때 함수.


2. R.when/R.unless는 조건이 만족하지 않으면 원본 값을 그대로 반환

이건 실수로 값을 가공하지 않았다고 오해할 수 있습니다. 항상 기대값 확인이 필요합니다.


3. R.cond는 R.T 를 써서 default 조건 을 꼭 넣어야 안전

조건에 맞는 항목이 없을 경우, 에러가 나거나 undefined가 나올 수 있습니다.

R.T는 항상 참이므로 “else” 역할을 합니다.


🧩 조합 예제: pipe + 조건

const adjustScore = R.pipe(
  R.when(R.lt(R.__, 60), R.always(60)),  // 최소 점수 보장
  grade                                   // 앞서 만든 등급 함수와 연결
);

adjustScore(50); // 'D'
adjustScore(85); // 'B'

✅ Call to Action

if와 switch에서 벗어나, 함수형 스타일로 조건을 표현해보세요.

조건도 함수로 다룰 수 있다는 것이 함수형 프로그래밍의 진짜 재미입니다.

실습 아이디어:

  • 금액이 10000 이상이면 할인 적용 (R.ifElse)

  • 배열이 비어있으면 “없음”, 아니면 첫 번째 항목 추출 (R.ifElse + R.isEmpty)

  • 나이에 따라 성인/청소년/어린이 구분 (R.cond)

728x90
728x90

🧱 Ramda 다섯 번째 스텝: 객체 다루기 – R.prop, R.path, R.assoc, R.evolve

함수형 프로그래밍에서는 객체를 직접 수정하지 않고, 복사된 새 객체를 만들며 다룹니다.
Ramda는 이를 돕기 위한 강력한 함수들을 제공합니다.

오늘 배울 함수들:

  • R.prop – 특정 key의 값을 가져오기
  • R.path – 깊숙한 nested 값 가져오기
  • R.assoc – 객체의 key를 업데이트 (immutable)
  • R.evolve – 여러 key에 대해 변환 적용

🧠 Topic Summary

불변(immutability)을 유지하면서 객체 다루기

JavaScript에서는 객체를 직접 변경할 수 있지만, 함수형 프로그래밍에서는 원본 객체를 건드리지 않고 새로운 객체를 만들어야 합니다.

Ramda의 함수들은 이 원칙을 지키면서도 객체를 쉽게 다룰 수 있도록 도와줍니다.


🔍 R.prop – 객체의 특정 필드 값을 가져오기

R.prop('name', { name: 'Alice' }); // 'Alice'
R.prop('age')({ name: 'Alice', age: 30 }); // 30

커리화 가능하므로 R.map(R.prop('name'))처럼 리스트에서 사용하기 좋습니다.

예제: 사용자 이름 목록 추출

const users = [
  { name: 'Alice' },
  { name: 'Bob' },
  { name: 'Charlie' }
];

R.map(R.prop('name'), users); 
// ['Alice', 'Bob', 'Charlie']

🌳 R.path – 깊은 속성 값 읽기

const user = { profile: { name: 'Alice' } };

R.path(['profile', 'name'], user); // 'Alice'
R.path(['profile', 'age'], user); // undefined

예제: 안전하게 중첩된 속성 읽기

const getUserCity = R.path(['address', 'city']);

getUserCity({ address: { city: 'Seoul' } }); // 'Seoul'
getUserCity({}); // undefined

🧩 R.assoc – 객체에 key-value 할당 (불변 방식)

R.assoc('age', 30, { name: 'Alice' }); 
// { name: 'Alice', age: 30 }

원본 객체는 변경되지 않고, 새 객체가 반환됩니다.

예제: 상태에 isLoading 값 추가

const setLoading = R.assoc('isLoading', true);

setLoading({ data: [] }); 
// { data: [], isLoading: true }

🔧 R.evolve – 여러 key를 동시에 변형하기

const user = { name: 'alice', age: 25 };

R.evolve({
  name: R.toUpper,
  age: R.inc
}, user);

// { name: 'ALICE', age: 26 }

예제: 상태 객체 업데이트

const state = { loading: false, count: 0 };

const nextState = R.evolve({
  loading: R.not,
  count: R.inc
})(state);

// { loading: true, count: 1 }

⚠️ Common Pitfalls

1. R.prop, R.path는 undefined를 반환할 수 있음

경로가 없을 경우 에러가 아니라 undefined를 반환하므로, 이를 체크해야 할 때는 R.pathOr 또는 R.defaultTo를 함께 사용하세요.

R.pathOr('unknown', ['user', 'name'], {}); // 'unknown'

2. assoc은 깊은 속성에 직접 접근하지 않음

R.assoc('user.name', 'Bob', {}) // ❌ 의도대로 작동하지 않음

이럴 땐 R.assocPath를 사용해야 합니다:

R.assocPath(['user', 'name'], 'Bob', {}); 
// { user: { name: 'Bob' } }

🧩 조합 예제: pipe와 함께 쓰기

const transformUser = R.pipe(
  R.evolve({
    name: R.toUpper,
    score: R.add(10)
  }),
  R.assoc('active', true)
);

transformUser({ name: 'alice', score: 80 }); 
// { name: 'ALICE', score: 90, active: true }

✅ Call to Action

Ramda의 객체 관련 함수들은 불변성과 안전성을 기반으로 동작합니다.

직접 R.prop, R.path, R.assoc, R.evolve를 활용해 상태 관리, 데이터 가공 코드를 작성해보세요.

실습 아이디어:

  • 주소 객체에서 zipcode를 안전하게 가져오는 함수 만들기

  • 사용자 정보 객체에서 이름은 대문자로, 나이는 1 증가시키기

  • 배열의 객체들에 isActive: true 필드를 추가하기 (R.map + R.assoc)

728x90

+ Recent posts