분류 전체보기
- 웹접근성 2025년 KWCAG2.2 신규 항목 정리 2025.01.13
- 웹접근성 2025년 KWCAG2.2 적용 심사관련 안내 (1월 시행) 2025.01.13
- 모바일 기기별 사이즈 2024.07.26
- vue3 vite 공통레이아웃 라우터 분기 처리 2024.03.26
- JavaScript에서 API 호출을 수행하는 4가지 방법 2024.03.26
- Laravel 9 Sanctum, Vue 3 및 Vite를 사용하여 SPA 인증을 구현하는 방법 2024.03.26
- npx 란? 2023.12.05 1
- React 생각 2023.12.04
- 프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법 2023.12.04 1
- (번역) React에서 UI와 로직 분리하기:헤드리스 컴포넌트를 사용한 클린 코드 접근법 2023.12.04 1
웹접근성 2025년 KWCAG2.2 신규 항목 정리
웹접근성 2025년 KWCAG2.2 적용 심사관련 안내 (1월 시행)
2025년 1월부터 웹 콘텐츠 접근성 품질인증 (WA) 심사항목을 현행 22개에서 33개로 확대 적용되어 심사 진행됨을 알려 드립니다.
2025년 1월 신청접수분 부터 KWCAG2.2의 심사지침이 적용됩니다.
※ 다만, 2024년 12월 31일까지 접수 예정인 사이트중 2025년으로 심사가 이월되는 경우 아래와 같이 적용됩니다.
-아 래 -
① 신규심사 : ∼24.12.31 신청접수분(∼25.1.31 심사착수분)까지 종전 기준적용
② 갱신심사 : 기존 인증만료일이 25.3.31 이내인 사이트 중 ∼24.12.31 신청접수분까지는 종전 기준적용
참고 사이트
'웹접근성' 카테고리의 다른 글
웹접근성 2025년 KWCAG2.2 신규 항목 정리 (0) | 2025.01.13 |
---|
모바일 기기별 사이즈
기기명 | phys. width |
phys. height |
CSS width |
CSS height |
pixel ratio |
phys. ppi |
OS | Launch Date |
Apple iPhone 14 Pro Max | 1290 | 2796 | 430 | 932 | 3 | 460 | iOS 16.0 | 2022.09 |
Apple iPhone 14 Plus | 1284 | 2778 | 428 | 926 | 3 | 458 | iOS 16.0 | 2022.09 |
Apple iPhone 14 Pro | 1179 | 2556 | 393 | 852 | 3 | 460 | iOS 16.0 | 2022.09 |
Apple iPhone 14 | 1170 | 2532 | 390 | 844 | 3 | 460 | iOS 16.0 | 2022.09 |
Apple iPhone 13 Pro Max |
1284 | 2778 | 428 | 926 | 3 | 458 | iOS 15.0 | 2021.09 |
Apple iPhone 13 mini | 1080 | 2340 | 360 | 780 | 3 | 476 | iOS 15.0 | 2021.09 |
Apple iPhone 13, 13 Pro | 1170 | 2532 | 390 | 844 | 3 | 460 | iOS 15.0 | 2021.09 |
Apple iPhone 12 Pro Max | 1284 | 2778 | 428 | 926 | 3 | 458 | iOS 14.1 | 2020.10 |
Apple iPhone 12 mini | 1080 | 2340 | 360 | 780 | 3 | 476 | iOS 14.1 | 2020.10 |
Apple iPhone 12, 12 Pro |
1170 | 2532 | 390 | 844 | 3 | 460 | iOS 14.1 | 2020.10 |
Apple iPhone SE (2세대) | 750 | 1334 | 375 | 667 | 2 | 326 | iOS 13.0 | 2020.04 |
Apple iPhone 11 Pro Max | 1242 | 2688 | 414 | 896 | 3 | 458 | iOS 13.0 | 2019.09 |
Apple iPhone 11 Pro | 1125 | 2436 | 375 | 812 | 3 | 458 | iOS 13.0 | 2019.09 |
Apple iPhone 11 | 828 | 1792 | 414 | 896 | 2 | 326 | iOS 13.0 | 2019.09 |
Apple iPhone Xs Max | 1242 | 2688 | 414 | 896 | 3 | 458 | iOS 12.0 | 2018.09 |
Apple iPhone Xr | 828 | 1792 | 414 | 896 | 2 | 326 | iOS 12.0 | 2018.09 |
Apple iPhone X, Xs | 1125 | 2436 | 375 | 812 | 3 | 458 | X : iOS 11.1.1 Xs : iOS 12.0 |
X : 2017.09 Xs : 2018.09 |
Apple iPhone 6+, 6s+, 7+, 8+ | 1080 | 1920 | 414 | 736 | 3 | 401 | ||
Apple iPhone 7, iPhone 8 | 750 | 1334 | 375 | 667 | 2 | 326 | 7 : iOS 10.0.1 8 : iOS 11.0 |
7 : 2016.09 8 : 2017.09 |
Apple iPhone SE | 640 | 1136 | 320 | 568 | 2 | 326 | iOS 9.3.2 | 2016.03 |
Apple iPhone 6, 6s | 750 | 1334 | 375 | 667 | 2 | 326 | 6 : iOS 8.0 6s : iOS 9.0 |
6 : 2014.09 6s : 2015.09 |
Apple iPhone 5 | 640 | 1136 | 320 | 568 | 2 | 326 | iOS 6.0 | 2012.09 |
Apple iPhone 4 | 640 | 960 | 320 | 480 | 2 | 326 | iOS 4.0 | 2010.06 |
Samsung Galaxy Z Fold2 | 1768 | 2208 | 884 | 1104 | 2 | 373 | Android 10.0 | 2020.08 |
Samsung Galaxy Z Flip | 1080 | 2636 | 412 | 1004 | 2.625 | 425 | Android 10.0 | 2020.02 |
Samsung Galaxy S22 Ultra | 1440 | 3088 | 360 | 772 | 4 | 500 | Android 12.0 | 2022.02 |
Samsung Galaxy S22, S22+ | 1080 | 2340 | 360 | 780 | 3 | S22 : 422 S22+ : 393 |
Android 12.0 | 2022.02 |
Samsung Galaxy S21 Ultra | 1440 | 3200 | 384 | 854 | 3.75 | 515 | Android 11.0 | 2021.01 |
Samsung Galaxy S21 | 1080 | 2400 | 360 | 800 | 3 | 421 | Android 11.0 | 2021.01 |
Samsung Galaxy S20 Ultra | 1440 | 3200 | 412 | 915 | 3.5 | 525 | Android 10.0 | 2020.02 |
Samsung Galaxy S20+ | 1440 | 3200 | 384 | 854 | 3.75 | 525 | Android 10.0 | 2020.02 |
Samsung Galaxy S20 | 1440 | 3200 | 360 | 800 | 4 | 563 | Android 10.0 | 2020.02 |
Samsung Galaxy S10+ | 1440 | 3040 | 412 | 869 | 3.5 | 522 | Android 9.0 | 2019.03 |
Samsung Galaxy S10 | 1440 | 3040 | 360 | 760 | 4 | 550 | Android 9.0 | 2019.02 |
Samsung Galaxy S9+ | 1440 | 2960 | 360 | 740 | 4 | 529 | Android 8.0 | 2018.03 |
Samsung Galaxy S9 | 1440 | 2960 | 360 | 740 | 4 | 570 | Android 8.0 | 2018.03 |
Samsung Galaxy S8+ | 1440 | 2960 | 360 | 740 | 4 | 529 | Android 7.0 | 2017.04 |
Samsung Galaxy S8 | 1440 | 2960 | 360 | 740 | 4 | 568 | Android 7.0 | 2017.04 |
Samsung Galaxy S7, S7 edge | 1440 | 2560 | 360 | 640 | 4 | S7 : 577.4 S7 edge : 534 |
Android 6.0 | 2016.02 |
Samsung Galaxy S6 | 1440 | 2560 | 360 | 640 | 4 | 577 | Android 5.0 | 2015.03 |
Samsung Galaxy S5 | 1080 | 1920 | 360 | 640 | 3 | 432 | Android 4.4 | 2014.02 |
Samsung Galaxy S4 | 1080 | 1920 | 360 | 640 | 3 | 441 | Android 4.2 | 2013.03 |
Samsung Galaxy S3 | 720 | 1280 | 360 | 640 | 2 | 306 | Android 4.0 | 2012.05 |
Samsung Galaxy S2 | 480 | 800 | 320 | 533 | 1.5 | 217 | Android 2.3 | 2011.02 |
Samsung Galaxy S | 480 | 800 | 320 | 533 | 1.5 | 233 | Android 2.1 | 2010.03 |
Samsung Galaxy Note 20 Ultra | 1440 | 3088 | 412 | 883 | 3.5 | 496 | Android 10.0 | 2020.08 |
Samsung Galaxy Note 20 | 1080 | 2400 | 412 | 915 | 2.625 | 393 | Android 10.0 | 2020.08 |
Samsung Galaxy Note 10+ | 1440 | 3040 | 412 | 869 | 3.5 | 498 | Android 9.0 | 2019.08 |
Samsung Galaxy Note 10 | 1080 | 2280 | 412 | 869 | 2.625 | 401 | Android 9.0 | 2019.08 |
Samsung Galaxy Note 9 | 1440 | 2960 | 414 | 846 | 3.5 | 516 | Android 8.1 | 2018.08 |
Samsung Galaxy Note 8 | 1440 | 2960 | 414 | 846 | 3.5 | 521 | Android 7.1.1 | 2017.09 |
Samsung Galaxy Note 7 | 1440 | 2560 | 360 | 640 | 4 | 518 | Android 6.0 | 2016.08 |
Samsung Galaxy Note 5 | 1440 | 2560 | 360 | 640 | 4 | 518 | Android 5.1 | 2015.08 |
Samsung Galaxy Note 4 | 1440 | 2560 | 360 | 640 | 4 | 518 | Android 4.4 | 2014.09 |
Samsung Galaxy Note 3 | 1080 | 1920 | 360 | 640 | 3 | 387 | Android 4.3 | 2013.09 |
Samsung Galaxy Note 2 | 720 | 1280 | 360 | 640 | 2 | 267 | Android 4.1 | 2012.08 |
Samsung Galaxy Note | 800 | 1280 | 400 | 640 | 2 | 285 | Android 2.3 | 2011.09 |
출처: https://circumeo.tistory.com/51 [℃:티스토리]
디테일하게 나온곳.
Viewport Size for Devices | Screen Sizes, Phone Dimensions and Device Resolution | YesViz.com
An Authentic Guide of Viewport Sizes for devices including Apple iPhone, Samsung, Tablets, Smart Watches and Android Phones. A detailed comparison list of Phone Dimensions, Screen Sizes and Device Resolution.
yesviz.com
''
논리적 픽셀
고밀도 디스플레이 기술로 디바이스마다 해상도가 높아졌고, 픽셀 밀도도 다양해졌다.
픽셀 밀도가 다양해지니 개별 픽셀의 크기도 천차만별이었다.
디바이스별 개별 픽셀의 크기에 따라 같은 이미지가 다른 위치, 크기로 보이는 문제가 발생했다.
그래서 어떤 디바이스에서든 같은 이미지가 동일하게 보이도록 논리적 픽셀을 도입했다.
*논리적 픽셀: css에서 표현하는 픽셀로, 물리적 픽셀의 토대 역할을 한다.
즉, 디스플레이 픽셀이 몇 개든 css 픽셀 컨텐츠가 구현된다.
보통 논리적 픽셀을 기준으로 디자인한 것을 디바이스 밀도에 맞게 몇 배로 곱한다.
논리적 픽셀 단위는
- 안드로이드 OS에서 DP와 SP
- 아이폰 OS에서 PT
*물리적 픽셀(px): 최종적으로 디바이스 화면에 출력되는 픽셀이다.
우리가 보는 디바이스의 실제 해상도를 이루는 픽셀이라 할 수 있다.
*DP: 안드로이드 OS의 논리적 픽셀 = 밀도 독립적 픽셀(Density Independent Pixel)이란 뜻
*SP: 안드로이드 환경의 폰트 단위 = 크기 독립적 픽셀(Scale Independent Pixel)이란 뜻
기본적으로 DP와 같은 크기지만, DP와 달리 SP는 시스템 폰트 설정 값에 영향을 받는다.
즉, 사용자가 안드로이드 시스템 설정에서 폰트 크기를 조절하면 SP 단위가 적용된 폰트도 조절되어
사용자들은 시스템 폰트를 크게 설정하기때문에 그러한 사용자를 고려한다면 sp를 사용하기도 한다.
ex) 높은 연령대,장애인, 근시,난시 등
*PT(Point): 아이폰 OS의 논리적 픽셀로, DP와 같은 역할을 한다.
AOS와 IOS 밀도
안드로이드 기반 디바이스는 고도로 발전하여 현재 점유율이 가장 높고 디스플레이 밀도도 매우 다양하다.
그래서 안드로이드 OS(AOS)에선 DPI를 6단계로 나누고 DP라는 논리적 픽셀을 공통의 측정 단위로 정했다.
기준 밀도(mdpi)인 160 dpi가 초기 시장에서는 가장 보편적이었으나, 이제는 초고밀도(xhdpi)인 320 dpi가 가장 보편적이다.

*안드로이드 배포 대시보드: developer.android.com/about/dashboards?hl=ko
*안드로이드 밀도 지원: developer.android.com/training/multiscreen/screendensities
한편 아이폰 OS(IOS)에서는 밀도를 3단계로 나누고 PT라는 논리적 픽셀을 공통의 측정 단위로 정했다.
아이폰 밀도 단계
mdpi: 1@x *base(기준 밀도)이며 1pt가 1px이다.
xhdpi: 2@x *1pt=2px
xxhdpi: 3@x *1pt=3px
안드로이드, 아이폰 OS의 밀도 단계에 대해 알아보았다.
디바이스 밀도에 따라 어떻게 디자인해야 하는지 알고 싶다면 다음의 포스팅을 참고하자.
UI·UX 디자이너를 위한 가이드 - 디바이스와 밀도 (깨지는 이미지)
'이 게시글은 공부하면서 정리한 내용입니다. 잘못된 정보는 제보해주시면 매우 감사드리겠습니다. *** 다양한 밀도에서 깨지는 픽셀 논리적 픽셀 도입 후 여러 디바이스에서 같은 이미지를 같
mesign.tistory.com
출처: https://mesign.tistory.com/11 [UI·UX Design Study:티스토리]
1 dp는 mdpi(160 dpi) 디스플레이에서 1px이며,
DPI에 따라 2px이나 3px, 또는 4px이 되기도 한다.
*보통 디자인 툴에서 1dp는 1px로 설정, 디자이너는 1dp로 디자인한 후 렌더링한다.
dp를 px로 변환식 = px = dp * (dpi / 160)
*래스터화 과정: 논리적 픽셀 → (n배수로 렌더링) → 렌더링 된 픽셀 → (다운/업샘플링) → 물리적 픽셀 →물리적 디바이스
*다운샘플링: 렌더링 된 픽셀 해상도가 물리적 디바이스의 해상도보다 높으면 크기를 낮춘다.
*업샘플링: 렌더링 된 픽셀 해상도가 물리적 디바이스의 해상도보다 낮으면 크기를 높인다.
샘플링 과정에서 해상도를 강제로 키우거나 줄이므로 픽셀이 열화될 수 있다.
*아이폰 해상도 변환 가이드: www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutions
*모든 단위 변환 웹사이트: https://pixplicity.com/dp-px-converter/
출처: https://mesign.tistory.com/m/10
'etc' 카테고리의 다른 글
npx 란? (1) | 2023.12.05 |
---|---|
Iframe 높이 조절 (0) | 2023.11.23 |
로그인 인증의 방식 (1) | 2023.11.23 |
퍼블리셔 면접 관련 질문 정리 (3) | 2023.11.20 |
on-demand Atomic CSS (unocss) (0) | 2023.11.16 |
vue3 vite 공통레이아웃 라우터 분기 처리
프로젝트 설정
VITE를 사용하여 새로운 Vue 3 프로젝트 만들기
npm init vite@latest your-project-name -- --template vue-ts
Vue 라우터 설치 및 구성
npm install vue-router@4 --save
// or vue cli plugin
vue add router
라우터 구성을 살펴보기 전에 세 개의 보기(페이지)를 생성하여 나중에 라우팅을 테스트하는 데 사용할 수 있도록 하십시오(추가: src -> views -> Home.vue , src -> views -> About.vue 및 src) -> 보기 -> Profile.vue )
// src/views/Home.vue
<template>
<v-container class="text-center">
<h1>Home</h1>
<v-btn class="pa-3" to="/about">Go To About</v-btn>
<br />
<v-btn color="primary" class="ma-3" to="/profile">Go To Profile</v-btn>
</v-container>
</template>
<script lang="ts" setup></script>
// src/views/About.vue
<template>
<v-container class="text-center">
<h1>About</h1>
<v-btn class="pa-3" to="/">Go To Home</v-btn>
<br />
<v-btn color="primary" class="ma-3" to="/profile">Go To Profile</v-btn>
</v-container>
</template>
<script lang="ts" setup></script>
// path: src/views/Profile.vue
<template>
<v-container class="text-center">
<h1>Hello, this my profile page</h1>
<v-btn to="/">Go To Home</v-btn>
</v-container>
</template>
<script lang="ts" setup>
</script>
Vue Router에 대한 별도의 구성 파일을 만들어 보겠습니다.
다음 경로에 router라는 새 폴더를 만듭니다: src -> router
애플리케이션 경로를 정의할 수 있는 새 파일(src -> router -> Route.ts)을 만들고 vue 라우터가 제대로 작동하는지 확인하기 위한 일부 테스트 경로를 정의합니다.
// path: src/router/routes.ts
import { RouteRecordRaw } from 'vue-router';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue'),
},
{
path: '/about',
name: 'About',
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue'),
},
{
path: '/profile',
name: 'Profile',
component: () =>
import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
},
];
export default routes;
라우터의 기본 구성 파일을 추가합니다: src -> router -> index.ts
// path: src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import routes from './routes';
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
main.ts로 이동하여 다음 줄을 추가하세요.
// path: src/main.ts
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';
// line 1: import the router
import router from './router';
const app = createApp(App);
// line 2: use the router
app.use(router);
app.mount('#app');
App.vue 파일로 이동하여 거기에 "RouterView" 인스턴스를 추가하세요.
// path: src/App.vue
<script setup lang="ts">
</script>
<template>
<!-- Add this line -->
<router-view />
</template>
출처 : https://blog.izem.dev/a-vue-3-users-guide-to-creating-complex-layouts#heading-expected-results
'vue' 카테고리의 다른 글
Laravel 9 Sanctum, Vue 3 및 Vite를 사용하여 SPA 인증을 구현하는 방법 (0) | 2024.03.26 |
---|
JavaScript에서 API 호출을 수행하는 4가지 방법
JavaScript에서 API 호출을 수행하는 4가지 방법
이번 튜토리얼에서는 자바스크립트로 API를 호출하는 방법을 보여드리겠습니다. JavaScript에서는 HTTP 요청을 작성하고 서버/데이터베이스에서 동적 데이터를 검색하는 방법을 아는 것이 정말 중요했습니다. JavaScript는 API와 상호 작용하기 위해 일부 내장 브라우저 개체와 일부 외부 오픈 소스 라이브러리를 제공합니다.
xmlHttp.html
let request = new XMLHttpRequest();
request.open("GET", "<a href=https://jsonplaceholder.typicode.com/users>https://jsonplaceholder.typicode.com/users</a>");
request.send();
request.onload = () => {
console.log(request);
if (request.status === 200) {
// by default the response comes in the string format, we need to parse the data into JSON
console.log(JSON.parse(request.response));
} else {
console.log(`error ${request.status} ${request.statusText}`);
}
};
fetch.html
async function getUsers() {
let response = await fetch(
"<a href=https://jsonplaceholder.typicode.com/users>https://jsonplaceholder.typicode.com/users</a>"
);
let data = await response.json();
return data;
}
getUsers().then(data => console.log(data));
axios.html
axios
.get("<a href=https://jsonplaceholder.typicode.com/users>https://jsonplaceholder.typicode.com/users</a>")
.then(response => {
console.log(response.data);
})
.catch(error => console.error(error));
jquery-ajax.html
$(document).ready(function() {
$.ajax({
url: "<a href=https://jsonplaceholder.typicode.com/users>https://jsonplaceholder.typicode.com/users</a>",
type: "GET",
success: function(result) {
console.log(result);
},
error: function(error) {
console.log(error);
}
});
});
소스 코드: https://github.com/jayanthbabu123/all-possible-ways-making-api-call-javascript
Laravel 9 Sanctum, Vue 3 및 Vite를 사용하여 SPA 인증을 구현하는 방법
Laravel 9 Sanctum, Bootstrap5, Vue 3 및 Vite를 사용하여 단일 페이지 애플리케이션에 대한 완전한 등록 및 로그인 기능을 만듭니다.
목차
- 1단계: Laravel 프로젝트 생성
- 2단계: 데이터베이스 세부 정보 구성
- 3단계: 설치 laravel/ui
- 4단계: Vue 3 설치
- 5단계: vitejs/plugin-vue 플러그인 설치
- 6단계: vite.config.js 파일 업데이트
- 7단계: vite.config.js에서 부트스트랩 경로 가져오기
- 8단계: NPM 종속성 설치
- 9단계: bootstrap.js 업데이트
- 10단계: JS 폴더에 Bootstrap 5 SCSS 가져오기
- 11단계: Vite Dev Server 시작
- 12단계: Laravel Sanctum 설치
- 13단계: Laravel Sanctum 구성
- 14단계: 데이터베이스 마이그레이션
- 15단계: 프런트엔드 설정
1단계: Laravel 프로젝트 생성
먼저 터미널을 열고 다음 명령을 실행하여 새로운 Laravel 프로젝트를 만듭니다.
composer create-project --prefer-dist laravel/laravel:^9.0 lara9sanctum-vue3-vite
또는 Laravel 설치 프로그램을 글로벌 컴포저 종속성으로 설치한 경우:
laravel new lara9sanctum-vue3-vite
2단계: 데이터베이스 세부 정보 구성
.env 데이터베이스 세부정보 열기 및 업데이트
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<DATABASE NAME>
DB_USERNAME=<DATABASE USERNAME>
DB_PASSWORD=<DATABASE PASSWORD>
3단계: 설치 laravel/ui
composer require laravel/ui
php artisan ui vue --auth
4단계: Vue 3 설치
이제 노드 모듈을 설치한 후 애플리케이션에 vue 3을 설치해야 합니다. 이를 위해서는 터미널 npm install vue@next vue-loader@next 에서 다음 명령을 실행하세요 . vue-loader는 단일 파일 구성 요소라는 형식으로 Vue 구성 요소를 작성할 수 있는 웹팩용 로더 입니다 . vue-loader@next는 웹팩이 SFC라는 단일 파일 구성 요소에서 Vue 구성 요소를 작성하기 위한 로더입니다.
npm install vue@next vue-loader@next
5단계: vitejs/plugin-vue 플러그인 설치
laravel 9 최신 릴리스에서는 laravel에 vue3 또는 vue를 설치하기 위해 vitejs/plugin-vue 플러그인을 설치합니다. 이 플러그인은 vite에서 vuejs 애플리케이션을 실행하는 데 필요한 종속성을 제공합니다. Vite는 코드를 Rollup과 함께 묶고 localhost:3000 포트를 실행하여 핫 새로 고침 기능을 제공하는 빌드 명령입니다.
npm i @vitejs/plugin-vue
6단계: vite.config.js 파일 업데이트
Vite는 최신 JavaScript 애플리케이션을 위한 모듈 번들러 입니다 . vite.config.js를 열고 다음 코드를 복사하여 붙여넣습니다. 파일 상단에 있는 vite에서 먼저 송장을 정의하고 laravel-vite-plugin도 가져옵니다. 여기에서 플러그인()은 js 및 CSS 파일의 경로를 취하고 애플리케이션에 대한 번들을 생성합니다. 플러그인 배열에 vue()를 추가해야 합니다.
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(),
laravel([
'resources/js/app.js',
]),
],
});
7단계: vite.config.js에서 부트스트랩 경로 가져오기
먼저 vite.config.js를 변경하고 부트스트랩 5 경로를 추가하고 resources/css/app.css를 제거해야 합니다.
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [
vue(),
laravel([
'resource/scss/app.scss',
'resources/js/app.js',
]),
],
resolve: {
alias: {
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
'@': '/resources/js',
}
},
});
8단계: NPM 종속성 설치
프런트엔드 종속 항목을 설치하려면 다음 명령어를 실행하세요.
npm install
9단계: bootstrap.js 업데이트
import 대신에 사용해야 합니다 require.
import loadash from 'lodash'
window._ = loadash
import * as Popper from '@popperjs/core'
window.Popper = Popper
import 'bootstrap'
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios'
window.axios = axios
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
/*import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
forceTLS: true
});*/
10단계: JS 폴더에 Bootstrap 5 SCSS 가져오기
이제 resources/js/app.js 에서 부트스트랩 5 SCSS 경로를 가져와야 합니다.
리소스/js/app.js
import './bootstrap';
import '../sass/app.scss'
11단계: Vite Dev Server 시작
이제 vue 3을 설치한 후 다음 명령을 실행하기 위해 vite용 개발 서버를 시작해야 하며 resources/js/app.js 파일과 resources/css/app.css 파일을 감시합니다. 또한 http://localhost:3000에서 vite 서버를 시작합니다. vite hot reload용이므로 브라우저에서 열 수 없으며 백그라운드에서 실행되고 js 및 CSS와 같은 애플리케이션 자산을 감시합니다.
npm run dev
12단계: Laravel Sanctum 설치
공식 Laravel 웹사이트에서 문서를 찾을 수 있습니다.
composer require laravel/sanctum
13단계: Laravel Sanctum 구성
config/sanctum.php 다음 코드를 열고 업데이트하세요.
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
프로덕션에 배포할 때 이를 변경해야 하므로 허용된 도메인 목록을 쉼표로 구분하여 파일 SANCTUM_STATEFUL_DOMAINS 에 추가하는 .env 것이 좋습니다.
파일을 열고 .env 다음 줄을 추가하세요
SANCTUM_STATEFUL_DOMAINS=localhost:<PORT NUMBER>
세션 드라이버 변경
에서 .env세션 드라이버를 file 으로 업데이트합니다 cookie.
SESSION_DRIVER=cookie
CORS 구성
다음 코드를 열고 config/cors.php 파일에 업데이트합니다.
'paths' => [
'api/*',
'/login',
'/logout',
'/sanctum/csrf-cookie'
],
또한 supports_credentials 옵션을 다음으로 설정하십시오 true.
'supports_credentials' => true,
로그인 양식을 보관하고 일부 비밀을 표시하는 Vue 구성 요소를 만들어 보겠습니다.
14단계: 데이터베이스 마이그레이션
php artisan migrate
15단계: 프런트엔드 설정
이전에 php artisan ui vue를 사용하여 프런트엔드 코드를 생성했을 때 예제 구성 요소가 resources/js/components/ExampleComponent.vue. 로그인, 등록 및 대시보드 페이지에 대한 다른 구성 요소를 만들어 보겠습니다.
Vue 라우터란 무엇인가요?
Vue Router는 브라우저의 URL/기록과 Vue의 구성 요소 간의 연결을 도와 특정 경로가 연관된 모든 뷰를 렌더링할 수 있도록 합니다.
Vue 라우터의 특징
- 중첩 경로
- 경로 매개변수, 쿼리
- 동적 경로 일치
- 자동 활성 CSS 클래스와의 링크
- 그리고 더 많은
설치하자 vue-router
npm install vue-router
이제 로그인 및 등록을 위한 구성 요소를 생성합니다.
Login.vue 를 사용하여 resources/js/comComponents 폴더 이름 안에 파일을 만듭니다 .
resources/js/components/Login.vue
<template>
<div class="container h-100">
<div class="row h-100 align-items-center">
<div class="col-12 col-md-6 offset-md-3">
<div class="card shadow sm">
<div class="card-body">
<h1 class="text-center">Login</h1>
<hr/>
<form action="javascript:void(0)" class="row" method="post">
<div class="col-12" v-if="Object.keys(validationErrors).length > 0">
<div class="alert alert-danger">
<ul class="mb-0">
<li v-for="(value, key) in validationErrors" :key="key">{{ value[0] }}</li>
</ul>
</div>
</div>
<div class="form-group col-12">
<label for="email" class="font-weight-bold">Email</label>
<input type="text" v-model="auth.email" name="email" id="email" class="form-control">
</div>
<div class="form-group col-12 my-2">
<label for="password" class="font-weight-bold">Password</label>
<input type="password" v-model="auth.password" name="password" id="password" class="form-control">
</div>
<div class="col-12 mb-2">
<button type="submit" :disabled="processing" @click="login" class="btn btn-primary btn-block">
{{ processing ? "Please wait" : "Login" }}
</button>
</div>
<div class="col-12 text-center">
<label>Don't have an account? <router-link :to="{name:'register'}">Register Now!</router-link></label>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name:"login",
data(){
return {
auth:{
email:"",
password:""
},
validationErrors:{},
processing:false
}
},
methods:{
...mapActions({
signIn:'auth/login'
}),
async login(){
this.processing = true
await axios.get('/sanctum/csrf-cookie')
await axios.post('/login',this.auth).then(({data})=>{
this.signIn()
}).catch(({response})=>{
if(response.status===422){
this.validationErrors = response.data.errors
}else{
this.validationErrors = {}
alert(response.data.message)
}
}).finally(()=>{
this.processing = false
})
},
}
}
</script>
Register.vue를 사용하여 resources/js/comComponents 폴더 이름 안에 파일을 생성하세요 .
<template>
<div class="container h-100">
<div class="row h-100 align-items-center">
<div class="col-12 col-md-6 offset-md-3">
<div class="card shadow sm">
<div class="card-body">
<h1 class="text-center">Register</h1>
<hr/>
<form action="javascript:void(0)" @submit="register" class="row" method="post">
<div class="col-12" v-if="Object.keys(validationErrors).length > 0">
<div class="alert alert-danger">
<ul class="mb-0">
<li v-for="(value, key) in validationErrors" :key="key">{{ value[0] }}</li>
</ul>
</div>
</div>
<div class="form-group col-12">
<label for="name" class="font-weight-bold">Name</label>
<input type="text" name="name" v-model="user.name" id="name" placeholder="Enter name" class="form-control">
</div>
<div class="form-group col-12 my-2">
<label for="email" class="font-weight-bold">Email</label>
<input type="text" name="email" v-model="user.email" id="email" placeholder="Enter Email" class="form-control">
</div>
<div class="form-group col-12">
<label for="password" class="font-weight-bold">Password</label>
<input type="password" name="password" v-model="user.password" id="password" placeholder="Enter Password" class="form-control">
</div>
<div class="form-group col-12 my-2">
<label for="password_confirmation" class="font-weight-bold">Confirm Password</label>
<input type="password_confirmation" name="password_confirmation" v-model="user.password_confirmation" id="password_confirmation" placeholder="Enter Password" class="form-control">
</div>
<div class="col-12 mb-2">
<button type="submit" :disabled="processing" class="btn btn-primary btn-block">
{{ processing ? "Please wait" : "Register" }}
</button>
</div>
<div class="col-12 text-center">
<label>Already have an account? <router-link :to="{name:'login'}">Login Now!</router-link></label>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name:'register',
data(){
return {
user:{
name:"",
email:"",
password:"",
password_confirmation:""
},
validationErrors:{},
processing:false
}
},
methods:{
...mapActions({
signIn:'auth/login'
}),
async register(){
this.processing = true
await axios.get('/sanctum/csrf-cookie')
await axios.post('/register',this.user).then(response=>{
this.validationErrors = {}
this.signIn()
}).catch(({response})=>{
if(response.status===422){
this.validationErrors = response.data.errors
}else{
this.validationErrors = {}
alert(response.data.message)
}
}).finally(()=>{
this.processing = false
})
}
}
}
</script>
인증된 모든 페이지에 대한 레이아웃 구성 요소를 만듭니다. 따라서 모든 페이지 구성 요소에 머리글, 바닥글 및 기타 구성 요소를 추가할 필요가 없으므로 여기에서는 Dashboard.vue라는 레이아웃 구성 요소를 만들었습니다. 여기 구성 요소에 머리글, 바닥글 및 라우터 보기를 추가하여 모든 구성 요소가 이 라우터 보기에서 렌더링되도록 합니다.
resources/js/components/layouts/Default.vue
import {mapActions} from 'vuex'
export default {
name:"default-layout",
data(){
return {
user:this.$store.state.auth.user
}
},
methods:{
...mapActions({
signOut:"auth/logout"
}),
async logout(){
await axios.post('/logout').then(({data})=>{
this.signOut()
this.$router.push({name:"login"})
})
}
}
}
resources/js/components/Dashboard.vue
<template>
<div class="container">
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header">
<h3>Dashboard</h3>
</div>
<div class="card-body">
<p class="mb-0">You are logged in as <b>{{user.email}}</b></p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name:"dashboard",
data(){
return {
user:this.$store.state.auth.user
}
}
}
</script>
이제 이 페이지 구성 요소를 라우터에 추가하세요.
새 파일 만들기 resources/js/router/index.js
import { createWebHistory, createRouter } from 'vue-router'
import store from '@/store'
/* Guest Component */
const Login = () => import('@/components/Login.vue')
const Register = () => import('@/components/Register.vue')
/* Guest Component */
/* Layouts */
const DahboardLayout = () => import('@/components/layouts/Default.vue')
/* Layouts */
/* Authenticated Component */
const Dashboard = () => import('@/components/Dashboard.vue')
/* Authenticated Component */
const routes = [
{
name: "login",
path: "/login",
component: Login,
meta: {
middleware: "guest",
title: `Login`
}
},
{
name: "register",
path: "/register",
component: Register,
meta: {
middleware: "guest",
title: `Register`
}
},
{
path: "/",
component: DahboardLayout,
meta: {
middleware: "auth"
},
children: [
{
name: "dashboard",
path: '/',
component: Dashboard,
meta: {
title: `Dashboard`
}
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes, // short for `routes: routes`
})
router.beforeEach((to, from, next) => {
document.title = to.meta.title
if (to.meta.middleware == "guest") {
if (store.state.auth.authenticated) {
next({ name: "dashboard" })
}
next()
} else {
if (store.state.auth.authenticated) {
next()
} else {
next({ name: "login" })
}
}
})
export default router
라우터 추가 resources/js/app.js
import './bootstrap';
import '../sass/app.scss'
import Router from '@/router'
import { createApp } from 'vue/dist/vue.esm-bundler';
const app = createApp({})
app.use(Router)
app.mount('#app')
이러한 요청을 하기 전에 API에 대한 기본 URL을 설정하고(이러한 내용은 현재 요청에 포함되어 있지 않음) 옵션을 활성화해야 합니다 withCredentials .
해당 파일을 열고 resources/js/bootstrap.js다음 코드를 추가합니다.
window.axios.defaults.withCredentials = true
여기서 an withCredentials 옵션은 정말 중요합니다. 이 Axios는 모든 요청과 함께 인증 쿠키를 자동으로 보내도록 지시합니다.
Vuex란 무엇인가요?
Vuex 는 Vue용 상태 관리 패턴 + 라이브러리입니다. js 애플리케이션. 상태가 예측 가능한 방식으로만 변경될 수 있도록 보장하는 규칙을 사용하여 애플리케이션의 모든 구성 요소에 대한 중앙 집중식 저장소 역할을 합니다.
글쎄요, 우리는 클라이언트에서 전체적으로 인증된 '상태'를 유지하고 싶기 때문에 여기서는 Vuex와 같은 상태 관리 라이브러리를 사용하는 것이 합리적입니다. 또한 인증 여부(예: 탐색)를 구성 요소 내에서 쉽게 확인할 수 있습니다.
설치하자 Vuex
npm install vuex --save
먼저 resources/js/store/auth.js 다음을 사용하여 파일을 만듭니다.
import axios from 'axios'
import router from '@/router'
export default {
namespaced: true,
state:{
authenticated:false,
user:{}
},
getters:{
authenticated(state){
return state.authenticated
},
user(state){
return state.user
}
},
mutations:{
SET_AUTHENTICATED (state, value) {
state.authenticated = value
},
SET_USER (state, value) {
state.user = value
}
},
actions:{
login({commit}){
return axios.get('/api/user').then(({data})=>{
commit('SET_USER',data)
commit('SET_AUTHENTICATED',true)
router.push({name:'dashboard'})
}).catch(({response:{data}})=>{
commit('SET_USER',{})
commit('SET_AUTHENTICATED',false)
})
},
logout({commit}){
commit('SET_USER',{})
commit('SET_AUTHENTICATED',false)
}
}
}
속성 state 에는 인증 여부가 포함되며, 인증되면 가져올 사용자 세부 정보가 포함됩니다.
우리는 getters 그 상태로 돌아왔습니다.
우리 mutations 의 업데이트는 state. 예를 들어, 성공적으로 인증되면 인증됨을 설정하기 위한 변형을 커밋 true 하고 사용자 세부 정보를 설정하기 위한 또 다른 변형을 커밋합니다.
때로는 브라우저 로컬 저장소에 일부 정보를 유지하기 위해 VueJS 웹 앱이 필요합니다. 로컬 설정, 계정 정보 또는 일부 토큰일 수 있습니다. 페이지가 새로 고쳐지면 우리는 그것들을 잃고 싶지 않습니다. 이것이 우리가 vuex-persistedstate를 사용해야 하는 이유입니다 .
설치하다 vuex-persistedstate
npm i vuex-persistedstate
이제 NET Framework의 Vuex에 인증 모듈을 추가하세요 resources/js/store/index.js.
import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import auth from '@/store/auth'
const store = createStore({
plugins:[
createPersistedState()
],
modules:{
auth
}
})
export default store
Vuex 추가 resources/js/app.js
import './bootstrap';
import '../sass/app.scss'
import Router from '@/router'
import store from '@/store'
import { createApp } from 'vue/dist/vue.esm-bundler';
const app = createApp({})
app.use(Router)
app.use(store)
app.mount('#app')
resources/views/welcome.blade.php를 열고 다음 코드를 교체하세요:
@vite(['resources/js/app.js'])
이제 web.php 및 api.php 경로 파일에 경로를 정의하십시오. 경로 폴더 로 이동하여 web.php 파일을 열고 다음 경로를 업데이트합니다.
경로 / web.php
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('{any}', function () {
return view('welcome');
})->where('any', '.*');
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
이제 프로젝트를 실행할 시간입니다.
php artisan serve
브라우저에서 localhost:<PORT NUMBER>를 엽니다 .
출처 : https://morioh.com/a/538596e08ccf/laravel-9-sanctum-vue-3-vite-spa
'vue' 카테고리의 다른 글
vue3 vite 공통레이아웃 라우터 분기 처리 (0) | 2024.03.26 |
---|
npx 란?
npx는 npm@5.2.0에서부터 사용할 수 있는 npm을 좀 더 편하게 사용할 수 있는 npm에서 제공하는 도구입니다.
Node.js 도구를 전역에 설치하지 않고도 실행할 수 있게 해줍니다.
npm을 패키지로 설치할 때 2가지 케이스가 있습니다.
- 전역으로 패키지를 설치하여 의존성 라이브러리들을 전체적으로 관리
- 특정 프로젝트에만 의존성 라이브러리 설치
이렇게 설치할때 대표적으로 3가지 문제가 생기기 시작했습니다.
1. 모듈이 업데이트되었는지 안되었는지 확인하기
모든 프로젝트마다 모듈을 재설치하는 것이 아닌, 한번 설치한 모듈을 그대로 사용하기 때문에 프로그래머가 의식해서 글로벌 모듈을 최신으로 재설치하지 않으면 확인이 힘들다.
2. 업데이트 시 다른 프로젝트에 영향이 간다.
프로젝트를 여러 개를 운영하는데 같은 모듈의 각각 다른 버전이 필요한 상황이 있을 수 있습니다.
글로벌 모듈은 1개이기 때문에 변경할 수가 없습니다.
= 대표적으로 노드버전으로 인한 이슈가 저는 제일 많이 생겼는데. 각 프로젝트마다 노드버전이 달랐고 그래서 노드버전을 바꿔줘야 하는 일이 제일 많았습니다.
3. cra 같은 보일러플레이트에 치명적인 이슈
리엑트 생성 cra의 경우 변경사항이 잦습니다. 업데이트 때마다 새로운 기능과 다양한 버그가 고쳐지기 때문에 최신 버전으로 유지해 주는 것이 좋은데 이렇게 되면 생성 시 기마다 버전이 다르게 되어서 생기는 문제가 생깁니다.
npx은 언제 사용할까?
- npm run-script를 사용하지 않고 로컬에 설치된 패키지를 사용할 경우
- 일회성 명령으로 패키지를 실행할 경우
- 특정 노드 버전의 스크립트를 실행할경우
- gist-based scripts를 실행할 경우
npm run-script 없이 로컬로 설치된 도구사용
{
"devDependencies": {
"test": "^1.1.0"
}
}
로컬 devDependencies 설치된 test 패키지를 바로 실행 가능합니다.
app npx test 메세지
일회성 명령 실행
npx는 우선적으로 로컬에 패키지가 설치되어 있는지 파악후 패키지가 존재하지않으면 npm레지스트리에서 해당 이름의 패키지가 자동으로 설치되고 호출됩니다.
npx으로 실행된 패키지는 실행하기 위해 설치되었다가 실행 후 삭제됩니다.
다른 Node.js 버전으로 실행
ㅁnpm 레지스트리에 node라는 패키지가 존재합니다. (node-bin 패키지는 deprecated 되었습니다.)
node패키지는 nvm, nave 또는 n과 같은 노드 버전 관리 도구를 사용하지 않고도 서로 다른 노드버전을 사용하여 노드를 사용할수 있습니다.
gist-based scripts를 공유할 경우
github의 gist에 스크립트를 올리고 실행할 때 유용하게 사용할수 있습니다. gist에 스크립트를 올리고, npx {gist url}만 실행하면 스크립트를 실행할수 있습니다.
'etc' 카테고리의 다른 글
모바일 기기별 사이즈 (0) | 2024.07.26 |
---|---|
Iframe 높이 조절 (0) | 2023.11.23 |
로그인 인증의 방식 (1) | 2023.11.23 |
퍼블리셔 면접 관련 질문 정리 (3) | 2023.11.20 |
on-demand Atomic CSS (unocss) (0) | 2023.11.16 |
React 생각
React스럽게 생각하기
React는 규모가 크고 빠른 웹 어플리케이션을 자바스크립트로 만들 때 최고의 방법이라고 생각합니다. Facebook과 Instagram도 React를 사용하였으며 앱이 커질 때 아주 유용했습니다.
React의 여러 멋진 점 중 하나는 앱을 만들면서 앱에 대한 생각을 만든다는 것입니다. 이 문서에서는 React를 사용하여 검색가능한 상품 데이터 테이블을 만들 때 어떤 생각 프로세스를 통하는 지 살펴봅니다.
모형 (Mock)으로 시작하기
이미 JSON API와 디자이너에게 받은 모형이 있다고 생각해봅시다. 모형은 이렇게 보입니다.
JSON API는 아래와 같은 몇몇 데이터를 반환합니다.
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
1단계: UI를 컴포넌트 계층으로 분리하기
가장 먼저 해야할 일은 모형의 각 컴포넌트 (와 서브컴포넌트)에 박스를 그리고 이름을 지어주는 것입니다. 만약 디자이너와 함께 일한다면 디자이너가 이미 이 작업을 끝냈을 수 있으니 한번 물어보세요! 디자이너의 포토샵 레이어 이름이 React 컴포넌트의 이름이 될 수도 있습니다.
그러나 어떤 것들을 컴포넌트로 만들어야할 지 어떻게 알 수 있을까요? 그냥 새 함수나 객체를 만들 지 말 지 결정하는 기준을 그대로 적용하세요. 그런 테크닉 중 하나는 단일 책임 원칙 (single responsibility principle) 이며, 이는 컴포넌트가 한가지의 작업만 하는 것이 이상적이라는 것입니다. 컴포넌트가 점점 커진다면 작은 서브컴포넌트들로 분리되어야합니다.
주로 JSON 데이터 모델을 유저에게 보여주게 되며, 만약 모델이 제대로 만들어져 있다면 UI (그리고 컴포넌트 구조도)는 제대로 매핑될 것입니다. 그 이유는 UI와 데이터 모델은 보통 인포메이션 아키텍쳐 (information architecture) 와 서로 깊게 연관되어있기 때문이며, 이 말은 UI를 컴포넌트로 세부화 시키는 것이 대부분 그렇게 대단한 일이 아니라는 것입니다. 각 컴포넌트가 데이터 모델의 한 조각을 나타내도록 분리하면 됩니다.
다섯개의 컴포넌트로 이루어진 간단한 앱을 하나 살펴봅시다. 각 컴포넌트가 표시하는 데이터를 이탤릭체로 표기했습니다.
- FilterableProductTable (orange): 예제 전체를 포함합니다
- SearchBar (blue): 모든 유저 입력 (user input) 을 받습니다
- ProductTable (green): 유저 입력 (user input) 을 기반으로 데이터 콜렉션 (data collection) 을 필터해서 보여줍니다.
- ProductCategoryRow (turquoise): 각 category 의 헤딩을 보여줍니다
- ProductRow (red): 각 product 행을 보여줍니다.
ProductTable 을 보면 “Name” 과 “Price” 레이블을 포함한 테이블 헤더는 해당 컴포넌트에 존재하지 않습니다. 이 건 선호의 문제이며 어느 쪽으로 선택할 지는 결정에 따릅니다. 이 예제에서는 ProductTable 이 ProductTable 의 책임인 데이터 콜렉션 렌더링의 일부이기 때문에 ProductTable 을 남겨두었습니다. 그러나 이 헤더가 복잡해지면 (즉 정렬을 위한 어포던스를 추가하는 등) 이 자체의 ProductTableHeader 컴포넌트를 만드는 것이 더 합리적일 것입니다.
이제 모형에서 컴포넌트를 확인하였으므로 이를 계층 구조로 나열해봅시다. 이 작업은 쉽습니다. 모형의 다른 컴포넌트에서 나타나는 컴포넌트는 계층 구조의 자식으로 나타냅니다.
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
2단계: React를 이용해 정적버전 만들기
CodePen에서 Thinking In React: Step 2를 살펴보세요.
이제 컴포넌트 계층구조가 만들어졌으니 앱을 실제로 구현해볼 시간입니다. 가장 쉬운 방법은 데이터 모델을 가지고 UI를 렌더링은 하지만 아무 동작도 하지 않는 버전을 만들어보는 것입니다. 이렇게 과정을 나누는 것이 좋은데 정적 버전을 만드는 것은 생각은 적게 필요하지만 타이핑은 많이 필요로 하고, 상호작용을 만드는 것은 생각은 많이 해야하지만 타이핑은 적게 필요로 하기 때문입니다. 나중에 더 살펴봅시다.
데이터 모델을 렌더하는 앱의 정적 버전을 만들기 위해 다른 컴포넌트를 재사용하는 컴포넌트를 만들고 props 를 이용해 데이터를 전달해줍니다. props 는 부모가 자식에게 데이터를 넘겨줄 떄 사용할 수 있는 방법입니다. 만약 state 에 대해 알고 있다면 정적 버전을 만들기 위해 state를 일절 사용하지 마세요 . state는 오직 상호작용을 위해, 즉 시간이 지남에 따라 데이터가 바뀌는 것에서 사용합니다. 앱의 정적 버전을 만들 때에는 필요없습니다.
앱을 만들 때 하향식 (top-down)이나 상향식 (bottom-up)으로 만들 수 있습니다. 다시 말해 계층 구조의 상층부에 있는 컴포넌트 (즉 FilterableProductTable 부터 시작합니다)부터 만들거나 하층부에 있는 컴포넌트 (ProductRow) 부터 만들 수도 있습니다. 간단한 예제에서는 보통 하향식으로 만드는 게 쉽지만 프로젝트가 커지면 상향식으로 만들고 테스트를 작성하면서 진행하는 것이 더 쉽습니다.
이 단계가 끝나면 데이터 렌더링을 위해 만들어진 재사용 가능한 컴포넌트의 라이브러리를 가지게 됩니다. 현재는 앱의 정적 버전이기 때문에 컴포넌트는 render() 메서드만 가지고 있을 것입니다. 계층구조의 최상단 컴포넌트 (FilterableProductTable)는 prop으로 데이터 모델을 받습니다. 데이터 모델을 변경하고 ReactDOM.render() 을 다시 호출하면 UI는 업데이트 됩니다. 어느 곳을 고쳐서 어떻게 UI가 업데이트되는 지 확인하는 일은 어렵지 않은데 지금은 크게 복잡한 부분이 없기 때문입니다. React의 단방향 데이터 플로우 (one-way data flow) (또는 단방향 바인딩 (one-way binding))는 앱을 모듈화 하기 좋고 빠르게 만들어줍니다.
이 단계를 실행하는 데 도움이 필요하다면 React 문서 를 참고하세요.
짧은 소개: Props vs State
React 에는 두가지 데이터 “모델”인 props와 state가 있습니다. 이 둘 사이의 차이점을 이해하는 것이 중요합니다. 만약 차이점이 제대로 기억나지 않는다면 공식 리액트 문서 를 살펴보세요.
3단계: UI state에 대한 최소한의 (하지만 완전한) 표현 찾아내기
UI를 상호작용하게 만드려면 기반 데이터 모델을 변경할 수 있는 방법이 있어야합니다. React는 state 로 이를 쉽게합니다.
앱을 올바르게 만들기 위해서는 앱에서 필요로 하는 변경가능한 state의 최소 집합을 생각해보아야 합니다. 여기서 핵심은 DRY: Don’t Repeat Yourself 원칙입니다. 어플리케이션이 필요로 하는 state를 완전하지만 작은 형태로 표현할 방법을 찾아내고 다른 모든 것은 필요할 때 state에서 계산하면 됩니다. 예를 들어 TODO 리스트를 만든다고 하면, TODO 아이템을 저장하는 배열만 유지하고 TODO 아이템의 갯수를 가지는 state를 별도로 가질 필요는 없습니다. 대신 TODO 갯수를 렌더링해야한다면 TODO 아이템 배열의 길이를 가져오면 됩니다.
예제 어플리케이션 내 데이터들을 생각해봅시다. 우리는 현재,
- 제품의 원본 목록
- 유저가 입력한 검색어
- 체크박스의 값
- 필터링된 제품 목록
각각 살펴보고 어떤 게 state가 되어야하는 지 살펴봅시다. 각 데이터에 대한 질문을 해볼 수 있습니다.
- 부모로부터 props를 통해 전달됩니까? 그러면 확실히 state가 아닙니다.
- 시간이 지나도 변하지 않나요? 그러면 확실히 state가 아닙니다
- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가요? 그렇다면 state가 아닙니다.
제품의 원본 목록은 props를 통해 전달되므로 state가 아닙니다. 검색어와 체크박스는 state로 볼 수 있는데 시간이 지남에 따라 변하기도 하면서 다른 것들로부터 계산될 수 없기 때문입니다. 그리고 마지막으로 필터링된 목록은 state가 아닌데 제품의 원본 목록과 검색어, 체크박스의 값을 조합해서 계산해낼 수 있기 때문입니다.
결국 state는
- 유저가 입력한 검색어
- 체크박스의 값
만 남습니다.
4단계: State가 어디에 있어야할 지 찾기
CodePen에서 Thinking In React: Step 4를 살펴보세요.
좋습니다. 이제 앱에서 최소한으로 필요하는 state가 뭔지 찾아냈습니다. 다음으로는 어떤 컴포넌트가 state를 변경하거나 소유 할 지 찾아야합니다.
기억하세요: React는 항당 컴포넌트 계층구조를 따라 아래로 내려가는 단방향 데이터 흐름을 따릅니다. 어떤 컴포넌트가 어떤 상태를 가져야하는 지 바로 결정하기 어려울 수 있습니다. 많은 초보자들이 이 부분을 가장 이해하기 어려워합니다 아래 과정을 따라해보세요.
어플리케이션이 가지는 각각의 state에 대해서
- state를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요
- 공통 오너 컴포넌트 (common owner component)를 찾으세요 (계층 구조 내에서 특정 state를 필요로하는 모든 컴포넌트들의 위에 있는 하나의 컴포넌트).
- 공통 오너 혹은 더 상위에 있는 컴포넌트가 state를 가져야합니다.
- state를 소유할 적절한 컴포넌트를 찾지 못하였다면, 단순히 state를 소유하는 컴포넌트를 하나 만들어서 공통 오너 컴포넌트의 상위 계층에 추가하세요.
이 전략을 어플리케이션에 적용해봅시다.
- ProductTable 은 state에 의존한 상품 리스트의 필터링해야하고 SearchBar 는 검색어와 체크박스의 상태를 표시해주어야합니다.
- 공통 오너 컴포넌트는 FilterableProductTable 입니다.
- 의미상으로도 FilterableProductTable 이 검색어와 체크박스의 체크 여부를 가지는 것이 타당합니다.
좋습니다. state를 FilterableProductTable 에 두기로 했습니다. 먼저 인스턴스 속성인 this.state = {filterText: '', inStockOnly: false} 를 FilterableProductTable 의 constructor 에 추가하여 어플리케이션의 초기 상태를 반영합니다. 그리고 나서 filterText 와 inStockOnly 를 ProductTable 와 SearchBar 에 prop으로 전달합니다. 마지막으로 이 props를 사용하여 ProductTable 의 행을 정렬하고 SearchBar 의 폼 필드 값을 설정하세요.
이제 어플리케이션의 동작을 볼 수 있습니다. filterText 를 "ball" 로 설정하고 앱을 새로고침 해보세요. 데이터 테이블이 올바르게 업데이트 된 것을 볼 수 있습니다.
5단계: 역방향 데이터 흐름 추가하기
CodePen에서 Thinking In React: Step 5를 살펴보세요.
지금까지 우리는 계층 구조 아래로 흐르는 props와 state의 함수로서 앱을 만들었습니다. 이제 다른 방향의 데이터 흐름을 만들어볼 시간입니다. 계층 구조의 깊숙한 곳에 있는 폼 컴포넌트에서 FilterableProductTable 의 state를 업데이트할 수 있어야합니다.
React는 이러한 데이터 흐름을 명시적으로 보이게 만들어서 프로그램이 어떻게 동작하는 지 쉽게 파악할 수 있게 하지만 전통적인 양방향 데이터 바인딩 (two-way data binding)과 비교하면 더 많은 타이핑을 필요로 합니다.
현재 버전 예제에서 타이핑을 하거나 체크박스를 체크하려고 하면 React가 사용자의 입력을 무시합니다. 이는 의도한 것인데 FilterableProductTable 에서 넘어온 state가 항상 input의 value prop과 동일하기 떄문입니다.
우리가 어떤 걸 원하는 지 생각해봅시다. 우리는 사용자가 폼을 변경할 때마다 사용자의 입력을 반영할 수 있도록 state를 업데이트하기를 원합니다. 컴포넌트는 그 자신의 state만 변경할 수 있기 때문에 FilterableProductTable 는 SearchBar 에 콜백을 넘겨서 state가 업데이트되어야 할 때마다 호출되도록 할 것입니다. 우리는 input에 onChange 이벤트를 사용해서 알림을 받을 수 있습니다. FilterableProductTable 에서 전달된 콜백은 setState() 를 호출할 것이며, 앱은 업데이트될 것입니다.
복잡하게 들리지만 코드에선 몇줄밖에 안됩니다. 그리고 앱 전체적으로 데이터가 흐르는 모습을 명시적으로 볼 수 있습니다.
출처 :https://reactjs-kr.firebaseapp.com/docs/thinking-in-react.html
'React' 카테고리의 다른 글
프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법 (1) | 2023.12.04 |
---|---|
(번역) React에서 UI와 로직 분리하기:헤드리스 컴포넌트를 사용한 클린 코드 접근법 (1) | 2023.12.04 |
useLayoutEffect (0) | 2023.11.27 |
React Hooks 소개 (2) | 2023.11.27 |
Create React App(CRA)로 시작하기 (2) | 2023.11.23 |
프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법
컴포넌트 분리
컴포넌트는 어떤 문맥이든 가장 작은 단위로서 언급됩니다.¹ 프로그램이나 모듈의 구성 단위로서도 그러합니다.² 그렇기 때문에 컴포넌트라고 하면 프로그램 언어 수준이 아닌 소프트웨어 디자인 수준에서 나눌 수 있는 가장 작은 단위를 의미합니다. 이 의미를 프론트엔드 라이브러리 또는 프레임워크에 적용하면, 웹 앱을 구성 하는 데 있어 가장 작은 단위가 됩니다. 실제로 우리가 많이 사용하는 React를 살펴보면 웹 앱을 구성하는 가장 작은 단위는 컴포넌트 입니다. 페이지를 만드는 것도 컴포넌트이고 아주 작은 요소를 만드는 것도 컴포넌트 입니다. 그렇기 때문에 우린 컴포넌트를 언제 그리고 어떻게 더 작은 단위로 나누어야 하는지 고민하게 됩니다. 그 과정에서 나온 Presentational Components와 Container Components는 가장 유명한 기준 중 하나이고 Atomic Design의 응용이 사용되고 있기도 합니다. 그럼에도 불구하고 컴포넌트를 언제 그리고 어떻게 나눠야 하는지 막막한 경우가 많습니다. 왜냐하면 종종 언급되곤 하는 방법들은 이해하기 쉽지만 적용하기가 어렵고 예외적인 상황이 많이 발생하기 때문입니다. 결국 적용하는 데 있어서 일관성이 깨지고 어느새 앱은 유지보수하기에 충분히 복잡해져 있습니다.
이 글은 컴포넌트의 분리라는 문제에 대해 현재 제가 갖고 있는 아이디어를 소개합니다. 따라서 시간이 지나보니 잘못 되었거나 더 개선될 수 있습니다. 그렇기 때문에 이 글은 기록 그리고 공유에 초점을 맞췄습니다.
전 프론트엔드 개발이 어렵다고 생각합니다. 마땅히 정해진 규칙은 없지만 갈수록 복잡성은 커져갑니다. 빠르게 변하는 기술이 프론트엔드 개발을 이해하는 데 어렵게 만든다는 건 이제 새삼스럽지도 않습니다. 프론트는 기획과 디자인, 백엔드와 인프라 등 모든 방법과 기술을 조합한 결과가 보여지는 곳입니다. HTML과 CSS, Javscript 그리고 React로 조금만 시간을 들이면 화면에 보이는 이쁘고 잘 동작하는 요소들을 만들던 프론트엔드는 갈수록 다루기 어려워지는 느낌입니다. 그렇기 때문에 잘 만들어야 합니다.
그럼 프론트엔드 개발을 잘 하기위한 방법 중 하나인 컴포넌트 분리에 대해 살펴보겠습니다.
언제 나눠야 할까?
컴포넌트를 만드는 기준, 즉 나누는 기준 중 가장 많이 선택되는 이유는 재사용성과 복잡성입니다.
코드가 재사용 될 가능성이 있다면 새로운 컴포넌트를 만드는 건 좋은 생각입니다. 코드가 복잡하다면 가독성을 개선하고 유지보수 할 수 있게 만들기 위해 컴포넌트를 분리할 수 있습니다.³
컴포넌트는 UI를 독립적이고 재사용 가능한 조각으로 나눌 수 있습니다. 그리고 각 조각을 독립적으로 고려할 수 있습니다.⁴
재사용 가능한 컴포넌트를 만들어 놓는 것은 더 큰 앱에서 작업할 때 두각을 나타냅니다. UI 일부가 여러 번 사용되거나 UI 일부가 자체적으로 복잡한 경우에는 별도의 컴포넌트로 만드는 게 좋습니다.⁵
컴포넌트를 만들 때 가장 많이 발생하는 실수 다섯가지⁶
1. 복잡한 컴포넌트를 만든다.
2. 하나의 컴포넌트에 여러 책임을 추가한다.
3. 몇몇 동작하는 부분을 결합하여 컴포넌트를 만든다.
4. 비지니스 로직을 컴포넌트에 추가한다.
아래에서는 이 내용들을 더 자세하게 살펴보고 분리하는 이유와 방법을 정리해보려고 합니다.
A. 재사용 가능한 컴포넌트
A-1. 재사용 가능하다는 것은 일반적이라는 것
재사용이 가능하다는 건 그만큼 일반적이라는 걸 의미합니다. 예를 들어보면, ‘탈것’ 이라는 건 ‘자동차’, ‘자전거’ 보다 일반적입니다. 이것은 포함 관계로 봤을 때 포괄적이라는 걸 의미합니다.

그리고 ‘탈것’은 조금 더 보편적인 속성을 갖습니다.

컴포넌트로 예를 들어보면, 두 개의 버튼 컴포넌트가 아래와 같이 있을 때
function Button(...) {
return <button>...</button>
}
function ButtonWithIcon(...) {
return (
<div>
<i>...</i>
<button>...</button>
</div>
);
}
<button>…</button> 은 모든 버튼을 포함하는 포괄적인 개념입니다. 즉, <button>…</button>이라는 요소는 Button과 ButtonIcon을 모두 포함합니다. 그리고 <button>…</button>은 Button과 ButtonWithIcon 보다 보편적인 속성을 갖습니다. 그렇기 때문에 ‘버튼’이라는 기능을 사용해야 하는 곳이라면 <button>…</button>이 가장 일반적이기 때문에 어디에든 사용할 수 있고 재사용 가능합니다. 반면 ButtonWithIcon은 그렇지 않습니다.⁷
우리는 컴포넌트의 재사용성을 고려할 때 일반적이라는 두 가지 측면 중 ‘속성이 보편적’인지 고민합니다. 즉, ‘다른 컴포넌트가 가져가서 사용할 수 있도록 보편적인 속성을 갖고 있는가?’를 고려합니다. 마치 아래 그림에서 빨간색으로 칠해진 부분을 만들어야 합니다.

재사성을 높이려면 더 많은 컴포넌트를 만족시켜야 하고 더욱 일반적이어야 합니다
그렇다면 어떻게 재사용 가능한 컴포넌트를 만들 수 있을지 구체적으로 살펴보겠습니다.
A-2. HTML 요소 측면에서의 재사용성
컴포넌트를 만드는 데 너무 집중을 하다보면 지금 다루고 있는게 HTML을 포함하고 있다는 사실을 까먹을 때가 종종 있습니다. 그래서 컴포넌트를 분리할 때 HTML 요소 측면에서 고려해야할 것을 놓칠 때가 있습니다.
function ListComponent(...) {
return (
<ul>
<li>
<h3>...</h3>
<p>...</p>
</li>
<li>
<h3>...</h3>
<p>...</p>
</li>
</ul>
);
}
이런 형태의 컴포넌트가 있고 분리할 필요가 있다고 가정해보겠습니다. 분리한 컴포넌트는 어떻게 생겨야 할까요?
function ItemComponent(...) {
return (
<li>
<h3>...</h3>
<p>...</p>
</li>
);
}
// 또는
function SomethingComponent(...) {
return (
<>
<h3>...</h3>
<p>...</p>
</>
);
}
다양한 의견이 있을 수 있지만 개인적으로 SomethingComponent와 같이 분리해야 한다고 생각합니다. ItemComponent는 ul과 같은 리스트 요소에서 사용하도록 강제되는 측면이 있는 반면, SomethingComponent는 리스트 요소 말고 다른 곳에서도 사용할 수 있기 때문입니다. 즉, 조금 더 보편적인 속성을 가져야 하고(li, h3, p가 아닌 h3, p), 포괄적이어야(리스트 아이템이 아니라 다른 무언가) 합니다. (사실 h3도 사용할 수 있는 맥락이 한정적이기 때문에 재사용성을 떨어뜨리는 원인 중 하나 입니다.)
그래서 전 ListComponent를 만들 때 li 요소를 블록으로 간주하지 않는 방법을 선호합니다. 즉, li 요소 바로 아래에 구성하는 게 아니라 다른 적당한 요소를 사용합니다.
function ListComponent(...) {
return (
<ul>
<li>
<!-- 다른 요소를 블록으로 활용합니다. -->
<section>
<h3>...</h3>
<p>...</p>
</section>
</li>
...
</ul>
);
}
그리고 요소의 구조를 잡을 때 BEM의 개념을 사용하곤 합니다. 왜냐하면 BEM의 Block은 독립적인 재사용성을 고려해야 하기 때문이고 이 개념은 컴포넌트를 분리할 때 기준으로서 도움이 됩니다.
따라서 컴포넌트를 분리할 때 HTML 요소들이 주변 문맥에 크게 의존적이지 않을수록 좋습니다.
A-3. 중복을 고려한 재사용성
우리가 컴포넌트의 재사용성에 대해 말할 땐 보통 중복을 떠올립니다. 두 곳 이상에서 중복된 무언가 존재하고 ‘추출’하는 과정을 거칩니다. 그리고 추출한 것을 재사용합니다. (추출이란 단어를 사용한 이유는 컴포넌트로 분리하는 과정이 이미 존재하는 컴포넌트에서 중복된 걸 골라내는, 즉 추출하는 과정과 비슷하기 때문입니다.)
function Page1() {
return (
<ul>
<li>
<section>
<h3>...</h3>
<p>가격...</p>
<p>요약...</p>
</section>
</li>
</ul>
);
}
function Page2() {
return (
<ul>
<li>
<section>
<h3>...</h3>
<p>가격...</p>
</section>
</li>
</ul>
);
}
이렇게 두 페이지에서 비슷한 형태의 요소 구성을 사용한다면 아래와 같이 컴포넌트로 추출 합니다.
function Page1() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}
function Page2() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}
그리고 추출한 Card 컴포넌트는 아래와 같이 만들어집니다.
function Card(props) {
return (
<section>
<h3>...</h3>
<p>가격...</p>
{props.showSummary && <p>요약...</p>}
</section>
);
}
이렇게 둘 이상의 컴포넌트에서 사용할 재사용 가능한 컴포넌트를 만들 때 가장 큰 특징 중 하나는 조건문 입니다. 완벽하게 같은 걸 사용하면 문제가 안 되지만 서로 다른 부분이 있다면 조건문이 들어가게 됩니다. 지금은 아주 간단한 컴포넌트라 문제가 잘 드러나지 않지만, 우리가 현실에서 마주하는 재사용 컴포넌트는 아래와 같이 점점 거대해지곤 합니다.
// 추출 후 1달
function Card(props) {
const [a, setA] = useState(props.a ? props.foo : props.bar);
const condition1 = props.a && !props.b;
return (
<section>
<h3>...</h3>
<p>가격...</p>
<div>
<button>{a ? 'fooValue' : 'barBalue'}</button>
</div>
{props.showSummary && <p>요약...</p>}
{condition1 && <div>...</div>}
</section>
);
}
// 더 미래는 생략...
이렇게 거대해지는 건 상위 컴포넌트와 하위 컴포넌트가 강하게 결합했기 때문입니다. 처음엔 아래 그림처럼 약하게 결합되었던 컴포넌트들이

시간이 지나면서 아래 그림의 두꺼워진 빨간 선처럼 강하게 결합된 것입니다.

강한 결합은 약한 결합보다 변경이 더 어렵다는 걸 의미합니다. 예를 들어, 처음 만들었을 땐 Card 컴포넌트를 다른 컴포넌트로 쉽게 바꿀 수 있었습니다. 또는 속성명을 쉽게 바꿀 수 있었습니다. 이젠 다른 컴포넌트로 바꾸거나 Card 컴포넌트의 속성 이름, 내부 로직을 바꾸기도 어렵습니다.
그리고 다른 문제도 있습니다. 화살표는 의존성을 의미합니다. Page1 컴포넌트와 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다. 의존적이라는 건 Card 컴포넌트를 수정하면 Page1과 Page2가 영향을 받기 때문에 혹시 문제가 발생하진 않는지 살펴봐야 한다는 걸 의미합니다.
만약 Page1과 Page2 뿐만 아니라 더 많은 컴포넌트가 Card 컴포넌트를 사용할수록 문제는 더 심각해집니다. 불행하게도 우린 모두 이런 문제에 익숙합니다. 이런 문제는 왜 발생했을까요? 그리고 어떤 문제를 일으킬까요?
가장 먼저 컴포넌트가 반환하는 요소의 중복을 추출해서 재사용해야 한다는 접근 방법이 문제의 발단일 수 있습니다. 중복 제거와 관련된 DRY 원칙⁸은 중복을 겉모습으로 판단하지 않습니다. 만약 모습이 같은 두 코드가 같은 이유로 수정된다면 그 코드는 중복입니다. 하지만 같은 모습의 코드라도 수정의 이유가 다르다면 두 코드는 중복이 아닙니다.
추출한 컴포넌트 내부에 사용하는 방법에 따라 조건문이 추가된다는 건, 사용하는 컴포넌트들이 서로 다른 수정의 이유를 갖는 다는 걸 의미합니다. 즉, 중복 제거와 재사용의 대상이 아닙니다. 따라서 처음에 조건문이 들어갈 때부터 산불의 작은 불씨가 시작된 것이었습니다.
물론 조건문이 없는 컴포넌트를 만들기는 너무나도 어렵습니다. 단, 조건문을 추가할 때 컴포넌트의 관계에 어떤 영향을 주는지 이해할 필요는 있습니다.
다음으로 의존성입니다. Page1과 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다. 즉, 여러 컴포넌트의 집합체인 상위 컴포넌트가 재사용을 위한 하위 컴포넌트에 의존적입니다. 이렇게 되면 Page1과 Page2 컴포넌트와 같은 상위 컴포넌트들은 변경의 이유가 너무 많아집니다. 즉, Page1의 Card에 대해 수정 요구사항이 들어왔을 때 Card 컴포넌트만 수정하는 게 아니라 Page2 컴포넌트도 수정해야할 수 있습니다.
이 문제들을 해결하는 방법 중 하나는 재사용하려는 컴포넌트에는 정말 공통적인 것들만 남겨두고 사용하는 컴포넌트의 고유한 것은 속성으로 전달하는 것입니다.
function Page1() {
return (
<ul>
<li>
<Card
summary={<p>요약...</p>}
/>
</li>
</ul>
);
}
function Page2() {
return (
<ul>
<li>
<Card ... />
</li>
</ul>
);
}
function Card(props) {
return (
<section>
<h3>...</h3>
<p>가격...</p>
{props.summary}
</section>
);
}
이렇게 하면 Page1만의 특징인 summay는 Page1이 관리하고 Card는 이에 대해 신경 쓸 필요가 없습니다. 이 말은 Page1에서 summary로 전달하는 요소를 수정해도 Page2 컴포넌트를 살펴볼 필요가 없다는 것을 의미합니다. 물론 현실 세계의 컴포넌트는 이렇게 호락호락하지 않습니다. 요소의 위치나 스타일 등의 상호 의존성도 존재할 수 있기 때문입니다. 하지만 상태나 조건문 등의 결합이 사라진 것만으로도 효과를 볼 수 있고, 개발을 하면서 신경쓸 부분이 객관적으로 줄었다는 건 분명 긍정적입니다. 특히나 이 방법은 props drilling을 피하거나 컴포넌트의 제어를 역전하는 등 좋은 점을 더 많이 갖고 있고 공식문서에서도 소개하는 만큼 반드시 숙지하고 있을 필요가 있습니다. 또한 이렇게 속성으로 컴포넌트를 전달해야 하는 이유와 방법을 잘 안내하고 있는 문서 링크를 공유합니다.
React component as prop: the right way
말씀드렸던 것처럼 이 방법은 결합도와 의존성 문제를 해결할 수 있는 방법 중 하나 입니다. 그렇기 때문에 재사용을 위해 컴포넌트를 분리한다면 어떤 점을 주의해야 하는지 따져보고 그에 맞는 해결책을 상황에 맞게 찾아보는 게 정말 중요합니다.
B. 복잡한 컴포넌트
컴포넌트를 나누는 중요한 또 다른 이유 중 하나는 컴포넌트의 복잡성입니다. 컴포넌트가 복잡해지는 이유는 여러가지가 있습니다. 여기에선 두 가지 경우를 살펴보려고 합니다.
B-1. 컴포넌트가 여러 책임을 갖는 경우
여러 책임을 갖는 컴포넌트는 컴포넌트 분리 없이 만든 거대한 페이지 컴포넌트와 비슷합니다.
function Page(props) {
// 선택한 탭을 변경하면 보여주는 내용을 변경합니다.
// 페이징을 다룹니다.
// 단어를 검색을 합니다.
// 검색 조건 토글을 다룹니다.
// 등등
}
이렇게 되면 기능 간에 결합이 강하게 발생해서 수정이 쉽지않습니다. 이건 마치 삼체문제와 비슷해서 서로 상호작용하는 기능이 많아지는 것보다 문제의 복잡성이 더 빨리 어려워진다는 것을 의미합니다.
그렇기 때문에 컴포넌트를 책임에 맞게 나눠서 문제를 단순화 해야 합니다. Page 컴포넌트가 탭, 검색, 페이징 그리고 이 정보들을 취합해 컨텐츠를 보여주는 등 모든 책임을 갖지 않도록 해야 합니다. 따라서 아래 그림처럼 컴포넌트를 분리해서 책임을 나눠야 합니다.

각 컴포넌트는 각자의 UI에 대한 책임(B)을 갖습니다. 그리고 컨텐츠 컴포넌트와의 소통은 A라는 경로를 통해서만 이뤄집니다. 탭 컴포넌트를 예로 들면
function Tab(...) {
// B
const [selectedTabName, setSelectedTabName] = useState('인기');
const handleSelectTab = (tabName) => {
// B
setSelectedTabName(tabName);
// A
컨텐츠 컴포넌트와 소통수단(tabName); // 예를 들어, Context API
};
return (
<div>
<button
type="button"
style={{ selectedTabName === '인기'라면 하이라이트 }}
onClick={() => handleSelectTab('인기')}
>
인기글
</button>
<button
type="button"
style={{ selectedTabName === '추천'이라면 하이라이트 }}
onClick={() => handleSelectTab('추천')}
>
추천글
</button>
</div>
);
}
와 같습니다. 그리고 분리를 통해 페이지 컴포넌트에 모두 모여있으면 발생할 수 있는 다른 컴포넌트와의 직접적인 상호작용(C)을 제거하거나 차단합니다. 이 방법은 페이지 컴포넌트가 아니라도 여러 컴포넌트에 적용될 수 있습니다. 이 방법은 캡슐화라고 할 수도 있습니다. 여러 책임이 한 곳에 모여있으면 각 컴포넌트가 서로 강하게 결합될 가능성이 높아집니다. 따라서 컴포넌트가 자신의 책임을 갖도록 분리하고 다른 컴포넌트와 상호작용 할 땐 정해진 방법으로 하는 게 좋습니다. 즉, 스스로와 관련된 변경은 각 컴포넌트가 책임짐으로써 감추고, 잘 변경되지 않는 외부와의 메시징은 제한하는 캡슐화는 컴포넌트를 관리하는 좋은 방법 중 하나입니다. 이런 경우 컴포넌트를 분리합니다.
B-2. 컴포넌트에 비지니스 로직이 있는 경우
이전에 적었던 글에서도 다뤘던 내용이지만, 일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도⁹가 다릅니다. 예를 들어, 상품 카드에 환불이 가능한지 표시하는 방법은 많고 수시로 바꿀 수 있습니다. 빨간색이었다가 회색이 될 수 있고 보여줬다가 안 보여줄 수도 있습니다. 그리고 이런 요청은 실제로 많이 발생합니다. 하지만 환불 가능한 조건을 변경하는 건 사업 초기에 한 번 정해지고 바뀌지 않을 수도 있습니다.

하지만 컴포넌트에 비지니스 로직이 포함되어있다면 빈번한 UI 변경에 따라 자주 영향을 받을 수 있습니다.

그리고 비지니스 로직은 현실 세계의 비지니스 규칙이기 때문에 영향을 자주 받고 변경에 노출되면 문제가 될 수 있습니다. 예를 들어, 컴포넌트 A는 환불 가능 여부에 따라 true 또는 false가 필요하고, 컴포넌트 B는 true 또는 false가 필요했다가 상품의 타입도 같이 필요해졌다면 비지니스 로직의 코드를 수정해야 합니다. 보통 비지니스 로직처럼 여러 군데에서 사용하는 로직은 일반적인 성질을 갖는 무언가(가장 대표적으로 함수)로 분리하기 때문에 이 이슈는 더 큰 문제가 됩니다. 어디에서 해당 로직이 사용되는지 조사하고 변경의 영향을 판단해야 하기 때문입니다. 로직을 사용하는 모든 컴포넌트를 고려하다보면 로직이 같은 함수를 이름만 바꿔서 또 만드는 등의 문제가 발생하기도 합니다.
function isRefundable(product) {
// Business Logic
return true or false;
}
// 이 함수를 아래와 같이 변경합니다.
function isRefundable(product) {
// Business Logic
return true or false;
}
function getRefundableWithType(product) {
// Business Logic
return {
type: product type,
isRefundable: true or false,
};
}
// 이 함수를 또 아래와 같이 변경합니다.
function refundable(product) {
// Business Logic
return true or false;
}
function isRefundable(product) {
return refundable(product);;
}
function getRefundableWithType(product) {
return {
type: refundable(product);,
isRefundable: true or false,
};
}
// 등등...
따라서 UI와 비지니스 로직을 적절하게 분리하는 건 소프트웨어를 오랫동안 유지보수 하는 데 있어서 아주 중요합니다. 이 내용은 이전에 작성했던 글을 참고하시거나 발표자료를 보시면 더 자세하게 확인할 수 있습니다. 이렇게 컴포넌트에 비지니스 로직이 있다면 로직을 분리하게 되고 그 과정에서 컴포넌트를 책임에 맞게 분리하는 과정이 포함될 수 있습니다.
C. 렌더링 퍼포먼스
재사용과 복잡성 이외에 컴포넌트를 분리하면 좋은 기준 중 하나는 렌더링 퍼포먼스 입니다. 하나의 컴포넌트 안에서 서로 영향을 주지 않는 상태가 여럿 있으면 발생하는 문제입니다.
function Page1() {
const [카드 호버 상태, set카드 호버 상태] = useState(false);
const [탭 호버 상태, set탭 호버 상태] = useState('none');
return (
...
<ul>탭</ul>
...
<ul>카드</ul>
...
);
}
이 코드에서 탭과 카드는 서로 영향을 주지 않습니다. 하지만 탭에 호버를 하면 카드들이 렌더링되고 카드에 호버를 하면 탭이 렌더링 됩니다. 그리고 카드들 같은 경우 각 카드도 보통은 호버 상태가 다른 카드의 호버 상태에 영향을 주지 않습니다. 따라서 아래와 같이 분리를 하면 좋습니다.
function Page1() {
return (
...
<Tab></Tab>
...
<ul>
...
<li><Card></Card><li>
...
</ul>
...
);
}
function Tab() {
const [탭 호버 상태, set탭 호버 상태] = useState('none');
return (
<ul>탭</ul>
);
}
function Card() {
const [카드 호버 상태, set카드 호버 상태] = useState(false);
return (
<section>...</section>
);
}
특히 input을 다루는 경우 상태 변경에 대한 업데이트가 다른 컴포넌트로 전파될 수 있기 때문에 주의해야 합니다.
마무리
지금까지 제가 리액트 컴포넌트를 분리하거나 다룰 때 사용하는 기준에 대해 살펴봤습니다. 당연하지만 이 기준들이 정답은 아닙니다. 프로젝트가 처한 상황, 개발 문화, 더 넓게는 회사의 상황이나 구성원들과의 커뮤니케이션 등 많은 문제가 연관되어 있을 수 있기 때문입니다. 소프트웨어를 만들고 유지보수 한다는 건 영문 타이핑 만큼 단순했지만 갈수록 더 복잡하고 고려할게 많아지는 것 같습니다. 이 방법들이 이 글을 읽으시는 분들의 환경에 맞게 고려되고 참고가 되면 좋겠습니다.
마지막으로 유연한 컴포넌트를 만드는 데 도움이 많이 됐던 글들을 소개합니다.
—
[1]: 로버트 C. 마틴, 클린 아키텍처, 인사이트, 100p
[2]: https://www.techtarget.com/whatis/definition/component
[3]: https://stackoverflow.com/questions/46192426/how-to-tell-when-to-create-a-new-component
[4]: https://vuejs.org/guide/essentials/component-basics.html
[5]: https://ko.reactjs.org/docs/components-and-props.html, https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children
[6]: https://blog.bitsrc.io/5-steps-to-build-react-components-like-a-pro-fb1f3af6ba17
[7]: 조영호, 오브젝트, 위키북스, 437p
[8]: 조영호, 오브젝트, 위키북스, 309p
[9]: 조영호, 오브젝트, 위키북스, 229p
'React' 카테고리의 다른 글
React 생각 (0) | 2023.12.04 |
---|---|
(번역) React에서 UI와 로직 분리하기:헤드리스 컴포넌트를 사용한 클린 코드 접근법 (1) | 2023.12.04 |
useLayoutEffect (0) | 2023.11.27 |
React Hooks 소개 (2) | 2023.11.27 |
Create React App(CRA)로 시작하기 (2) | 2023.11.23 |
(번역) React에서 UI와 로직 분리하기:헤드리스 컴포넌트를 사용한 클린 코드 접근법
프런트엔드 개발 영역에서는 용어와 패러다임이 때로는 이해하기 어려울 수 있으며 ‘헤드리스 UI’ 또는 ‘헤드리스 컴포넌트’도 이 범주에 속할 수 있습니다. 이러한 용어들이 무엇을 의미하는지 궁금해서 고개를 갸웃거리고, 혼자만 그런 것이 아닙니다. 사실, 혼란스러운 이름에도 불구하고 이러한 개념들은 복잡한 사용자 인터페이스 관리를 상당히 단순화할 수 있는 매력적인 전략입니다.
헤드리스 컴포넌트는 난해해 보일 수 있지만, 그 진정한 힘은 유연성, 재사용 가능성, 그리고 코드베이스의 구성과 깔끔함을 향상시킬 수 있는 능력에 있습니다. 이 글에서는 이 패턴이 정확히 무엇인지, 왜 유용한지, 그리고 인터페이스 디자인에 대한 접근 방식을 어떻게 혁신할 수 있는지에 대해 탐구해 볼 것입니다.
설명을 위해, 먼저 헤드리스 컴포넌트를 간단하면서도 효과적으로 적용하는 방법, 즉 두 개의 유사한 컴포넌트에서 ‘useToggle’ 훅을 추출하여 코드 중복을 줄이는 방법을 살펴보겠습니다. 이 예시는 사소해 보일 수 있지만, 헤드리스 컴포넌트의 핵심 원칙을 이해하는 데 도움이 됩니다. 공통 패턴을 인식하고 이를 재사용 가능한 부분으로 추출함으로써, 코드베이스를 간소화하고 더 효율적인 개발 과정을 위한 기반을 마련할 수 있습니다.
하지만 이것은 빙산의 일각에 불과합니다! 더 깊게 파고들면, 우리는 이 원칙이 실제로 적용되는 더 복잡한 사례를 만나게 될 것입니다. 그것은 향상된 입력 컴포넌트를 생성하기 위한 강력한 라이브러리인 Downshift를 활용하는 것입니다.
이 글을 모두 읽고나서, 헤드리스 컴포넌트에 대한 이해뿐만 아니라, 이 강력한 패턴을 자신의 프로젝트에 통합할 수 있는 자신감도 얻을 수 있길 바랍니다. 이제, 헤드리스 컴포넌트에 대한 혼란을 뒤로하고 혁신적인 잠재력에 대해서 알아봅시다.
토글 컴포넌트
토글은 수많은 애플리케이션에서 필수적인 부분을 차지합니다. “이 기기에서 내 정보 기억”, “알림 활성화” 또는 늘 인기 있는 “다크 모드”와 같은 기능을 뒤에서 조용히 수행합니다.
(사진: 토글 컴포넌트)
React에서 이러한 토글을 만드는 것은 놀라울 정도로 간단한 과정입니다. 어떻게 구현할 수 있는지 살펴보겠습니다.
const ToggleButton = () => {
const [isToggled, setIsToggled] = useState(false);
const toggle = useCallback(() => {
setIsToggled((prevState) => !prevState);
}, []);
return (
<div className="toggleContainer">
<p>Do not disturb</p>
<button onClick={toggle} className={isToggled ? "on" : "off"}>
{isToggled ? "ON" : "OFF"}
</button>
</div>
);
};
useState 훅은 초기 값이 false인 상태 변수 isToggled를 설정합니다. useCallback으로 생성된 toggle 함수는 호출될 때마다 (버튼 클릭 시) isToggled 값을 true와 false 사이에서 전환합니다. 버튼의 모양과 텍스트(“ON” 또는 “OFF”)는 isToggled 상태를 동적으로 반영합니다.
이제 완전히 다른 컴포넌트인 ExpandableSection을 만들어야 한다고 가정해 보겠습니다. 이 컴포넌트는 섹션의 세부 정보를 표시하거나 숨깁니다. 제목 옆에 버튼이 있으며, 클릭하여 세부 정보를 펼치거나 접을 수 있습니다.
(사진: ExpandableSection 컴포넌트)
구현도 그리 어렵지 않습니다. 아래와 같이 쉽게 할 수 있습니다.
const ExpandableSection = ({ title, children }: ExpandableSectionType) => {
const [isOpen, setIsOpen] = useState(false);
const toggleOpen = useCallback(() => {
setIsOpen((prevState) => !prevState);
}, []);
return (
<div>
<h2 onClick={toggleOpen}>{title}</h2>
{isOpen && <div>{children}</div>}
</div>
);
};
두 예제 사이에는 명백한 유사성이 있습니다. ToggleButton의 ‘on’ 과 ‘off’ 상태는 ExpandableSection의 ‘펼치기(expand)’ 와 ‘접기(collapse)’ 작업과 유사합니다. 이러한 공통점을 인식하면, 이 공통 기능을 별도의 기능으로 추상화할 수 있습니다. React 생태계에서는 사용자 정의 훅을 생성하여 이를 수행합니다.
const useToggle = (init = false) => {
const [state, setState] = useState(init);
const toggle = useCallback(() => {
setState((prevState) => !prevState);
}, []);
return [state, toggle];
};
리팩터링은 상당히 간단해 보일 수 있지만, 표현(UI)에서 동작을 분리한다는 중요한 개념을 강조합니다. 이 시나리오에서, 사용자 정의 훅은 JSX로부터 독립된 상태 머신 역할을 합니다. ToggleButton과 ExpandableSection 모두 이 동일한 기본 로직을 활용합니다.
중간 규모의 프론트엔드 프로젝트에 상당한 시간을 투자해 본 사람들은, 대부분의 업데이트나 버그가 UI 시각적 요소와 관련된 것이 아니라 UI 상태 관리와 관련된 로직에 문제가 있다는 것을 알게 될 것입니다. 훅은 이러한 로직을 중앙 집중화하는 강력한 도구를 제공하여, 코드 분석, 최적화 그리고 유지보수를 더욱 쉽게 만듭니다.
헤드리스 컴포넌트
실제로 이 패턴을 사용하여 동작(또는 상태 관리)과 표현을 분리하는 훌륭한 라이브러리가 이미 많이 있습니다. 그리고 이러한 컴포넌트 라이브러리 중 가장 유명한 것은 Downshift 일 것입니다.
Downshift는 UI를 렌더링하지 않고 동작과 상태를 관리하는 헤드리스 컴포넌트의 개념을 적용합니다. render 속성 함수에서 상태와 일련의 액션을 제공하여 UI에 연결할 수 있게 합니다. 이러한 방식으로, Downshift는 복잡한 상태와 접근성 관리를 담당하는 동시에 UI 제어를 가능하게 해줍니다.
예를 들어, 드롭다운 목록을 만들고 싶다면, 당연히 목록의 데이터, 트리거, 그리고 선택한 항목을 강조하는 방법, 렌더링할 라인 수에 대한 몇 가지 사용자 정의가 필요합니다. 하지만 크로스 브라우저 및 크로스 디바이스를 포함하여 고려해야 할 수많은 예외 사항이 있기 때문에 접근성을 처음부터 구축하고 싶지 않습니다.
(사진: StateSelect 컴포넌트)
Downshift를 사용하면 몇 줄의 JSX만으로도 접근성 있는 select를 쉽게 만들 수 있습니다.
const StateSelect = () => {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({items: states});
return (
<div>
<label {...getLabelProps()}>Issued State:</label>
<div {...getToggleButtonProps()} className="trigger" >
{selectedItem ?? 'Select a state'}
</div>
<ul {...getMenuProps()} className="menu">
{isOpen &&
states.map((item, index) => (
<li
style={
highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
}
key={`${item}${index}`}
{...getItemProps({item, index})}
>
{item}
</li>
))}
</ul>
</div>
)
}
이 컴포넌트는 Downshift의 useSelect 훅을 사용하는 상태 선택자입니다. 이를 통해 사용자는 드롭다운 메뉴에서 상태를 선택할 수 있습니다.
- useSelect 는 선택 입력에 대한 상태와 상호 작용을 관리합니다.
- isOpen, selectedItem, highlightedIndex 는 useSelect 에 의해 제어되는 상태 변수입니다.
- getToggleButtonProps, getLabelProps, getMenuProps, getItemProps 는 해당 요소에 필요한 속성을 제공하는 함수입니다.
- isOpen 은 드롭다운이 열려 있는지 여부를 결정합니다.
- selectedItem 은 현재 선택된 상태의 값을 보유합니다.
- highlightedIndex 는 현재 강조 표시된 목록 항목을 나타냅니다.
- 드롭다운이 열려 있으면, states.map은 선택 가능한 상태의 정렬되지 않은 목록을 생성합니다.
- 스프레드 (...) 연산자는 Downshift의 훅에서 컴포넌트에 props를 전달하는 데 사용됩니다. 여기에는 클릭 핸들러, 키보드 탐색 및 ARIA 속성과 같은 것들이 포함됩니다.
- 상태가 선택되면 버튼 내용으로 표시됩니다. 그렇지 않으면 ‘Select a state’라고 표시됩니다.
이 접근 방식은 렌더링에 대한 완전한 제어를 제공하므로, 애플리케이션의 모양과 느낌에 맞게 컴포넌트를 스타일링하고 필요한 경우 사용자 정의 동작을 적용할 수 있습니다. 또한 다양한 컴포넌트나 프로젝트 간에 동작 로직을 공유하는 데도 매우 좋습니다.
이미 이 패턴을 따르고 있는 헤드리스 컴포넌트 라이브러리도 몇 가지 더 있습니다.
- Reakit: 접근 가능한 고급 UI 라이브러리, 툴킷, 디자인 시스템 등을 구축하기 위한 헤드리스 컴포넌트 세트를 제공합니다.
- React Table: 조립하기 위한 헤드리스 유틸리티입니다. 훅 기반이며 모든 종류의 테이블을 만들 수 있습니다.
- react-use: 여러 헤드리스 컴포넌트가 포함된 훅의 모음입니다.
좀 더 깊이 파보기
의도적으로 UI에서 로직을 계속 분리해 나가면, 점차적으로 계층 구조가 형성됩니다. 이 구조는 전체 애플리케이션에 걸쳐있는 기존의 계층형 아키텍처가 아니라 애플리케이션의 UI 일부에 한정된 구조입니다.
(사진: 헤드리스 UI 패턴)
이 배치에서 JSX(또는 대부분의 태그)는 최상위 계층에서 정의되며, 이 계층은 전달된 속성을 표시하는 것만을 담당합니다. 바로 아래에는 ‘헤드리스 컴포넌트’라고 불리는 것이 있습니다. 이 컴포넌트는 모든 동작을 유지하고 상태를 관리하며 JSX와 상호 작용할 인터페이스를 제공합니다. 이 구조의 기반에는 도메인별 로직을 캡슐화하는 데이터 모델이 있습니다. 이러한 모델들은 UI 또는 상태와 관련이 없습니다. 대신, 데이터 관리와 비즈니스 로직에 중점을 둡니다. 이 계층적 접근법은 문제를 깔끔하게 분리하여 코드의 명확성과 유지 관리성을 향상시킵니다.
균형 잡힌 시각
다른 유형의 기술과 마찬가지로 헤드리스 UI도 채택하기 전에 알아야 할 장단점이 있습니다. 먼저 헤드리스 UI의 이점에 대해 논의해 보겠습니다.
- 재사용성:헤드리스 컴포넌트의 주요 장점은 재사용성입니다. 로직을 독립적인 컴포넌트로 캡슐화함으로써, 여러 UI 요소에서 이러한 컴포넌트를 재사용할 수 있습니다. 이는 코드 중복을 줄일 뿐만 아니라 애플리케이션 전반에 걸쳐 일관성을 강화합니다.
- 관심사 분리: 헤드리스 컴포넌트는 로직과 표현을 명확하게 분리합니다. 이로 인해 코드베이스가 더 관리하기 쉽고 이해하기 쉬워지며, 특히 업무가 분산된 대규모 팀에게 유용합니다.
- 유연성: 헤드리스 컴포넌트는 프레젠테이션에 구애받지 않기 때문에 디자인 유연성을 높일 수 있습니다. 기본 로직에 영향을 주지 않고 원하는 만큼 UI를 사용자 정의할 수 있습니다.
- 테스트 가능성: 프레젠테이션과 로직이 분리되어 있기 때문에, 비즈니스 로직에 대한 단위 테스트 작성이 더 쉽습니다.
반면, 일체형 컴포넌트보다 조금 더 복잡하기 때문에 다음 고려 사항을 염두에 두고 현명하게 사용해야 합니다.
- 초기 부담: 더 단순한 애플리케이션 또는 컴포넌트의 경우, 헤드리스 컴포넌트를 생성하는 것이 과도한 엔지니어링처럼 느껴져 불필요한 복잡성을 초래할 수 있습니다.
- 학습 곡선: 이 개념에 익숙하지 않은 개발자들은 처음에 이해하기 어려울 수 있으며, 학습 곡선이 더 가파르게 느껴질 수 있습니다.
- 남용 가능성: 모든 컴포넌트를 헤드리스로 만들려고 하다 보면, 불필요한 경우에도 과도하게 사용하게 되어 코드베이스가 지나치게 복잡해질 수 있습니다.
- 잠재적 성능 문제: 일반적으로 큰 문제는 아니지만, 신중하게 처리하지 않으면 공통 로직을 사용하여 여러 컴포넌트를 다시 렌더링하는 것이 성능 문제로 이어질 수 있습니다.
기억하세요, 헤드리스 UI는 다른 아키텍처 패턴처럼 모든 것에 적용되는 만능 해결책이 아닙니다. 이를 사용할지 여부는 프로젝트의 특정 요구 사항과 복잡성을 기반으로 결정되어야 합니다.
요약
이 글에서, 복잡한 UI 작업을 처리하는 강력한 접근법인 헤드리스 UI의 세계를 탐구했습니다. 렌더링을 분리하는 것이 어떻게 더 유지보수가 쉽고 재사용 가능한 코드를 생성할 수 있게 해주며, 중복성과 잠재적인 버그를 줄일 수 있는지 살펴보았습니다. 먼저 useToggle이라는 사용자 정의 리액트 훅을 생성하고 두 개의 별도의 컴포넌트에서의 적용을 보여주는 간단한 예를 통해 이를 설명했습니다. 그런 다음 향상된 입력 컴포넌트 구현을 용이하게 하는 뛰어난 라이브러리인 Downshift와 함께 이 개념을 보다 복잡한 시나리오로 확장했습니다. ‘헤드리스’ 접근법에 대한 더 깊은 이해를 통해, 향후 프로젝트에서 더 확장 가능하고 유지보수가 쉬운 UI를 만드는데 이 패턴을 활용할 수 있기를 바랍니다.
'React' 카테고리의 다른 글
React 생각 (0) | 2023.12.04 |
---|---|
프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법 (1) | 2023.12.04 |
useLayoutEffect (0) | 2023.11.27 |
React Hooks 소개 (2) | 2023.11.27 |
Create React App(CRA)로 시작하기 (2) | 2023.11.23 |