[{"data":1,"prerenderedAt":1860},["ShallowReactive",2],{"post-document:\u002Ffrontend\u002Fvue\u002F2026\u002F08\u002F03\u002Fvue-component\u002F":3},{"id":4,"title":5,"body":6,"categories":1816,"date":1819,"description":1820,"extension":1821,"image":1822,"key_concepts":1823,"last_modified_at":1822,"legacyPath":1827,"meta":1828,"navigation":96,"part":1822,"path":1848,"published":96,"robots":1822,"seo":1849,"series":1822,"stem":1850,"strengths":1822,"summary":1851,"tags":1852,"tradeoffs":1822,"__hash__":1859},"posts\u002Fposts\u002FFrontend\u002FVue\u002F2026-08-03-vue-component.md","04. Vue 컴포넌트 설계: props, emit, slot과 생명주기",{"type":7,"value":8,"toc":1802},"minimark",[9,14,18,29,36,222,226,232,299,307,310,406,409,413,416,478,483,492,691,725,738,742,745,874,1007,1010,1016,1020,1026,1029,1035,1134,1184,1188,1194,1265,1268,1310,1313,1317,1320,1436,1505,1509,1512,1577,1709,1727,1731,1734,1756,1759,1763,1796,1799],[10,11,13],"h2",{"id":12},"컴포넌트란","컴포넌트란?",[15,16,17],"p",{},"Vue 컴포넌트는 화면과 동작을 독립적이고 재사용 가능한 단위로 나눈 것이다. 한 컴포넌트가 하나의 명확한 책임을 가지면 테스트와 변경이 쉬워진다.",[19,20,26],"pre",{"className":21,"code":23,"language":24,"meta":25},[22],"language-text","App\n├── AppHeader\n├── ProductList\n│   └── ProductCard\n└── ShoppingCart\n","text","",[27,28,23],"code",{"__ignoreMap":25},[15,30,31,32,35],{},"Vue에서는 보통 ",[27,33,34],{},".vue"," 파일 하나에 템플릿, 로직, 스타일을 작성한다. 이를 SFC(Single-File Component)라고 한다.",[19,37,41],{"className":38,"code":39,"language":40,"meta":25,"style":25},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nconst title = '상품 목록'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Csection class=\"product-list\">\n    \u003Ch2>{{ title }}\u003C\u002Fh2>\n  \u003C\u002Fsection>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.product-list {\n  padding: 1rem;\n}\n\u003C\u002Fstyle>\n","vue",[27,42,43,63,81,91,98,108,128,143,153,162,167,180,189,207,213],{"__ignoreMap":25},[44,45,48,52,56,60],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sVt8B","\u003C",[44,53,55],{"class":54},"s9eBZ","script",[44,57,59],{"class":58},"sScJk"," setup",[44,61,62],{"class":50},">\n",[44,64,66,70,74,77],{"class":46,"line":65},2,[44,67,69],{"class":68},"szBVR","const",[44,71,73],{"class":72},"sj4cs"," title",[44,75,76],{"class":68}," =",[44,78,80],{"class":79},"sZZnC"," '상품 목록'\n",[44,82,84,87,89],{"class":46,"line":83},3,[44,85,86],{"class":50},"\u003C\u002F",[44,88,55],{"class":54},[44,90,62],{"class":50},[44,92,94],{"class":46,"line":93},4,[44,95,97],{"emptyLinePlaceholder":96},true,"\n",[44,99,101,103,106],{"class":46,"line":100},5,[44,102,51],{"class":50},[44,104,105],{"class":54},"template",[44,107,62],{"class":50},[44,109,111,114,117,120,123,126],{"class":46,"line":110},6,[44,112,113],{"class":50},"  \u003C",[44,115,116],{"class":54},"section",[44,118,119],{"class":58}," class",[44,121,122],{"class":50},"=",[44,124,125],{"class":79},"\"product-list\"",[44,127,62],{"class":50},[44,129,131,134,136,139,141],{"class":46,"line":130},7,[44,132,133],{"class":50},"    \u003C",[44,135,10],{"class":54},[44,137,138],{"class":50},">{{ title }}\u003C\u002F",[44,140,10],{"class":54},[44,142,62],{"class":50},[44,144,146,149,151],{"class":46,"line":145},8,[44,147,148],{"class":50},"  \u003C\u002F",[44,150,116],{"class":54},[44,152,62],{"class":50},[44,154,156,158,160],{"class":46,"line":155},9,[44,157,86],{"class":50},[44,159,105],{"class":54},[44,161,62],{"class":50},[44,163,165],{"class":46,"line":164},10,[44,166,97],{"emptyLinePlaceholder":96},[44,168,170,172,175,178],{"class":46,"line":169},11,[44,171,51],{"class":50},[44,173,174],{"class":54},"style",[44,176,177],{"class":58}," scoped",[44,179,62],{"class":50},[44,181,183,186],{"class":46,"line":182},12,[44,184,185],{"class":58},".product-list",[44,187,188],{"class":50}," {\n",[44,190,192,195,198,201,204],{"class":46,"line":191},13,[44,193,194],{"class":72},"  padding",[44,196,197],{"class":50},": ",[44,199,200],{"class":72},"1",[44,202,203],{"class":68},"rem",[44,205,206],{"class":50},";\n",[44,208,210],{"class":46,"line":209},14,[44,211,212],{"class":50},"}\n",[44,214,216,218,220],{"class":46,"line":215},15,[44,217,86],{"class":50},[44,219,174],{"class":54},[44,221,62],{"class":50},[10,223,225],{"id":224},"지역-등록과-전역-등록","지역 등록과 전역 등록",[15,227,228,231],{},[27,229,230],{},"\u003Cscript setup>","에서 컴포넌트를 import하면 현재 컴포넌트의 템플릿에서 바로 사용할 수 있다.",[19,233,235],{"className":38,"code":234,"language":40,"meta":25,"style":25},"\u003Cscript setup>\nimport BaseButton from '.\u002Fcomponents\u002FBaseButton.vue'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CBaseButton \u002F>\n\u003C\u002Ftemplate>\n",[27,236,237,247,261,269,273,281,291],{"__ignoreMap":25},[44,238,239,241,243,245],{"class":46,"line":47},[44,240,51],{"class":50},[44,242,55],{"class":54},[44,244,59],{"class":58},[44,246,62],{"class":50},[44,248,249,252,255,258],{"class":46,"line":65},[44,250,251],{"class":68},"import",[44,253,254],{"class":50}," BaseButton ",[44,256,257],{"class":68},"from",[44,259,260],{"class":79}," '.\u002Fcomponents\u002FBaseButton.vue'\n",[44,262,263,265,267],{"class":46,"line":83},[44,264,86],{"class":50},[44,266,55],{"class":54},[44,268,62],{"class":50},[44,270,271],{"class":46,"line":93},[44,272,97],{"emptyLinePlaceholder":96},[44,274,275,277,279],{"class":46,"line":100},[44,276,51],{"class":50},[44,278,105],{"class":54},[44,280,62],{"class":50},[44,282,283,285,288],{"class":46,"line":110},[44,284,113],{"class":50},[44,286,287],{"class":54},"BaseButton",[44,289,290],{"class":50}," \u002F>\n",[44,292,293,295,297],{"class":46,"line":130},[44,294,86],{"class":50},[44,296,105],{"class":54},[44,298,62],{"class":50},[15,300,301,302,306],{},"이런 ",[303,304,305],"strong",{},"지역 등록","을 기본으로 사용하면 의존성이 파일에 드러나고, 사용하지 않는 컴포넌트를 빌드 도구가 제거하기 쉽다.",[15,308,309],{},"앱 전체에서 반복하는 매우 기본적인 컴포넌트는 전역 등록할 수도 있다.",[19,311,315],{"className":312,"code":313,"language":314,"meta":25,"style":25},"language-js shiki shiki-themes github-light github-dark","import { createApp } from 'vue'\nimport App from '.\u002FApp.vue'\nimport BaseButton from '.\u002Fcomponents\u002FBaseButton.vue'\n\nconst app = createApp(App)\n\napp.component('BaseButton', BaseButton)\napp.mount('#app')\n","js",[27,316,317,329,341,351,355,370,374,391],{"__ignoreMap":25},[44,318,319,321,324,326],{"class":46,"line":47},[44,320,251],{"class":68},[44,322,323],{"class":50}," { createApp } ",[44,325,257],{"class":68},[44,327,328],{"class":79}," 'vue'\n",[44,330,331,333,336,338],{"class":46,"line":65},[44,332,251],{"class":68},[44,334,335],{"class":50}," App ",[44,337,257],{"class":68},[44,339,340],{"class":79}," '.\u002FApp.vue'\n",[44,342,343,345,347,349],{"class":46,"line":83},[44,344,251],{"class":68},[44,346,254],{"class":50},[44,348,257],{"class":68},[44,350,260],{"class":79},[44,352,353],{"class":46,"line":93},[44,354,97],{"emptyLinePlaceholder":96},[44,356,357,359,362,364,367],{"class":46,"line":100},[44,358,69],{"class":68},[44,360,361],{"class":72}," app",[44,363,76],{"class":68},[44,365,366],{"class":58}," createApp",[44,368,369],{"class":50},"(App)\n",[44,371,372],{"class":46,"line":110},[44,373,97],{"emptyLinePlaceholder":96},[44,375,376,379,382,385,388],{"class":46,"line":130},[44,377,378],{"class":50},"app.",[44,380,381],{"class":58},"component",[44,383,384],{"class":50},"(",[44,386,387],{"class":79},"'BaseButton'",[44,389,390],{"class":50},", BaseButton)\n",[44,392,393,395,398,400,403],{"class":46,"line":145},[44,394,378],{"class":50},[44,396,397],{"class":58},"mount",[44,399,384],{"class":50},[44,401,402],{"class":79},"'#app'",[44,404,405],{"class":50},")\n",[15,407,408],{},"전역 등록이 많아지면 어디에서 컴포넌트가 왔는지 찾기 어려워지므로 제한적으로 사용한다.",[10,410,412],{"id":411},"컴포넌트의-공개-인터페이스","컴포넌트의 공개 인터페이스",[15,414,415],{},"재사용 가능한 컴포넌트는 입력과 출력을 명확하게 만든다.",[417,418,419,435],"table",{},[420,421,422],"thead",{},[423,424,425,429,432],"tr",{},[426,427,428],"th",{},"기능",[426,430,431],{},"방향",[426,433,434],{},"역할",[436,437,438,452,465],"tbody",{},[423,439,440,446,449],{},[441,442,443],"td",{},[27,444,445],{},"props",[441,447,448],{},"부모 → 자식",[441,450,451],{},"데이터와 설정을 전달",[423,453,454,459,462],{},[441,455,456],{},[27,457,458],{},"emit",[441,460,461],{},"자식 → 부모",[441,463,464],{},"사용자 행동과 변경 의도를 알림",[423,466,467,472,475],{},[441,468,469],{},[27,470,471],{},"slot",[441,473,474],{},"부모 → 자식 내부",[441,476,477],{},"자식의 특정 위치에 콘텐츠를 넣음",[479,480,482],"h3",{"id":481},"props-부모가-데이터를-전달","props: 부모가 데이터를 전달",[15,484,485,488,489,491],{},[27,486,487],{},"ProductCard","는 어떤 상품을 보여줄지 직접 찾지 않고 ",[27,490,445],{},"로 받는다. 그래야 같은 컴포넌트로 여러 상품을 표현할 수 있다.",[19,493,495],{"className":38,"code":494,"language":40,"meta":25,"style":25},"\u003C!-- ProductCard.vue -->\n\u003Cscript setup>\nconst props = defineProps({\n  product: {\n    type: Object,\n    required: true\n  }\n})\n\nconst logProduct = () => {\n  console.log(props.product.id)\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle>\n    \u003Ch3>{{ product.name }}\u003C\u002Fh3>\n    \u003Cp>{{ product.price.toLocaleString() }}원\u003C\u002Fp>\n    \u003Cbutton @click=\"logProduct\">확인\u003C\u002Fbutton>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n",[27,496,497,503,513,528,533,538,546,551,556,560,577,588,592,600,604,612,622,636,650,673,682],{"__ignoreMap":25},[44,498,499],{"class":46,"line":47},[44,500,502],{"class":501},"sJ8bj","\u003C!-- ProductCard.vue -->\n",[44,504,505,507,509,511],{"class":46,"line":65},[44,506,51],{"class":50},[44,508,55],{"class":54},[44,510,59],{"class":58},[44,512,62],{"class":50},[44,514,515,517,520,522,525],{"class":46,"line":83},[44,516,69],{"class":68},[44,518,519],{"class":72}," props",[44,521,76],{"class":68},[44,523,524],{"class":58}," defineProps",[44,526,527],{"class":50},"({\n",[44,529,530],{"class":46,"line":93},[44,531,532],{"class":50},"  product: {\n",[44,534,535],{"class":46,"line":100},[44,536,537],{"class":50},"    type: Object,\n",[44,539,540,543],{"class":46,"line":110},[44,541,542],{"class":50},"    required: ",[44,544,545],{"class":72},"true\n",[44,547,548],{"class":46,"line":130},[44,549,550],{"class":50},"  }\n",[44,552,553],{"class":46,"line":145},[44,554,555],{"class":50},"})\n",[44,557,558],{"class":46,"line":155},[44,559,97],{"emptyLinePlaceholder":96},[44,561,562,564,567,569,572,575],{"class":46,"line":164},[44,563,69],{"class":68},[44,565,566],{"class":58}," logProduct",[44,568,76],{"class":68},[44,570,571],{"class":50}," () ",[44,573,574],{"class":68},"=>",[44,576,188],{"class":50},[44,578,579,582,585],{"class":46,"line":169},[44,580,581],{"class":50},"  console.",[44,583,584],{"class":58},"log",[44,586,587],{"class":50},"(props.product.id)\n",[44,589,590],{"class":46,"line":182},[44,591,212],{"class":50},[44,593,594,596,598],{"class":46,"line":191},[44,595,86],{"class":50},[44,597,55],{"class":54},[44,599,62],{"class":50},[44,601,602],{"class":46,"line":209},[44,603,97],{"emptyLinePlaceholder":96},[44,605,606,608,610],{"class":46,"line":215},[44,607,51],{"class":50},[44,609,105],{"class":54},[44,611,62],{"class":50},[44,613,615,617,620],{"class":46,"line":614},16,[44,616,113],{"class":50},[44,618,619],{"class":54},"article",[44,621,62],{"class":50},[44,623,625,627,629,632,634],{"class":46,"line":624},17,[44,626,133],{"class":50},[44,628,479],{"class":54},[44,630,631],{"class":50},">{{ product.name }}\u003C\u002F",[44,633,479],{"class":54},[44,635,62],{"class":50},[44,637,639,641,643,646,648],{"class":46,"line":638},18,[44,640,133],{"class":50},[44,642,15],{"class":54},[44,644,645],{"class":50},">{{ product.price.toLocaleString() }}원\u003C\u002F",[44,647,15],{"class":54},[44,649,62],{"class":50},[44,651,653,655,658,661,663,666,669,671],{"class":46,"line":652},19,[44,654,133],{"class":50},[44,656,657],{"class":54},"button",[44,659,660],{"class":58}," @click",[44,662,122],{"class":50},[44,664,665],{"class":79},"\"logProduct\"",[44,667,668],{"class":50},">확인\u003C\u002F",[44,670,657],{"class":54},[44,672,62],{"class":50},[44,674,676,678,680],{"class":46,"line":675},20,[44,677,148],{"class":50},[44,679,619],{"class":54},[44,681,62],{"class":50},[44,683,685,687,689],{"class":46,"line":684},21,[44,686,86],{"class":50},[44,688,105],{"class":54},[44,690,62],{"class":50},[19,692,694],{"className":38,"code":693,"language":40,"meta":25,"style":25},"\u003C!-- 부모 컴포넌트 -->\n\u003CProductCard :product=\"selectedProduct\" \u002F>\n",[27,695,696,701],{"__ignoreMap":25},[44,697,698],{"class":46,"line":47},[44,699,700],{"class":501},"\u003C!-- 부모 컴포넌트 -->\n",[44,702,703,705,707,710,713,715,718,721,723],{"class":46,"line":65},[44,704,51],{"class":50},[44,706,487],{"class":54},[44,708,709],{"class":50}," :",[44,711,712],{"class":58},"product",[44,714,122],{"class":50},[44,716,717],{"class":79},"\"",[44,719,720],{"class":50},"selectedProduct",[44,722,717],{"class":79},[44,724,290],{"class":50},[15,726,727,730,731,734,735,737],{},[27,728,729],{},"defineProps()","와 ",[27,732,733],{},"defineEmits()","는 ",[27,736,230],{},"에서 사용하는 컴파일러 매크로이므로 import하지 않는다. props는 읽기 전용이며 자식이 직접 수정하지 않는다.",[479,739,741],{"id":740},"emit-자식이-변경-의도를-알린다","emit: 자식이 변경 의도를 알린다",[15,743,744],{},"자식은 부모의 상태를 직접 바꾸는 대신 이벤트를 발생시킨다.",[19,746,748],{"className":38,"code":747,"language":40,"meta":25,"style":25},"\u003C!-- ProductCard.vue -->\n\u003Cscript setup>\ndefineProps({\n  product: {\n    type: Object,\n    required: true\n  }\n})\n\nconst emit = defineEmits(['add-to-cart'])\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"emit('add-to-cart', product.id)\">\n    장바구니에 담기\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[27,749,750,754,764,771,775,779,785,789,793,797,818,826,830,838,853,858,866],{"__ignoreMap":25},[44,751,752],{"class":46,"line":47},[44,753,502],{"class":501},[44,755,756,758,760,762],{"class":46,"line":65},[44,757,51],{"class":50},[44,759,55],{"class":54},[44,761,59],{"class":58},[44,763,62],{"class":50},[44,765,766,769],{"class":46,"line":83},[44,767,768],{"class":58},"defineProps",[44,770,527],{"class":50},[44,772,773],{"class":46,"line":93},[44,774,532],{"class":50},[44,776,777],{"class":46,"line":100},[44,778,537],{"class":50},[44,780,781,783],{"class":46,"line":110},[44,782,542],{"class":50},[44,784,545],{"class":72},[44,786,787],{"class":46,"line":130},[44,788,550],{"class":50},[44,790,791],{"class":46,"line":145},[44,792,555],{"class":50},[44,794,795],{"class":46,"line":155},[44,796,97],{"emptyLinePlaceholder":96},[44,798,799,801,804,806,809,812,815],{"class":46,"line":164},[44,800,69],{"class":68},[44,802,803],{"class":72}," emit",[44,805,76],{"class":68},[44,807,808],{"class":58}," defineEmits",[44,810,811],{"class":50},"([",[44,813,814],{"class":79},"'add-to-cart'",[44,816,817],{"class":50},"])\n",[44,819,820,822,824],{"class":46,"line":169},[44,821,86],{"class":50},[44,823,55],{"class":54},[44,825,62],{"class":50},[44,827,828],{"class":46,"line":182},[44,829,97],{"emptyLinePlaceholder":96},[44,831,832,834,836],{"class":46,"line":191},[44,833,51],{"class":50},[44,835,105],{"class":54},[44,837,62],{"class":50},[44,839,840,842,844,846,848,851],{"class":46,"line":209},[44,841,113],{"class":50},[44,843,657],{"class":54},[44,845,660],{"class":58},[44,847,122],{"class":50},[44,849,850],{"class":79},"\"emit('add-to-cart', product.id)\"",[44,852,62],{"class":50},[44,854,855],{"class":46,"line":215},[44,856,857],{"class":50},"    장바구니에 담기\n",[44,859,860,862,864],{"class":46,"line":614},[44,861,148],{"class":50},[44,863,657],{"class":54},[44,865,62],{"class":50},[44,867,868,870,872],{"class":46,"line":624},[44,869,86],{"class":50},[44,871,105],{"class":54},[44,873,62],{"class":50},[19,875,877],{"className":38,"code":876,"language":40,"meta":25,"style":25},"\u003C!-- 부모 컴포넌트 -->\n\u003Cscript setup>\nconst addToCart = (productId) => {\n  console.log('추가할 상품:', productId)\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CProductCard\n    :product=\"selectedProduct\"\n    @add-to-cart=\"addToCart\"\n  \u002F>\n\u003C\u002Ftemplate>\n",[27,878,879,883,893,916,930,934,942,946,954,961,977,994,999],{"__ignoreMap":25},[44,880,881],{"class":46,"line":47},[44,882,700],{"class":501},[44,884,885,887,889,891],{"class":46,"line":65},[44,886,51],{"class":50},[44,888,55],{"class":54},[44,890,59],{"class":58},[44,892,62],{"class":50},[44,894,895,897,900,902,905,909,912,914],{"class":46,"line":83},[44,896,69],{"class":68},[44,898,899],{"class":58}," addToCart",[44,901,76],{"class":68},[44,903,904],{"class":50}," (",[44,906,908],{"class":907},"s4XuR","productId",[44,910,911],{"class":50},") ",[44,913,574],{"class":68},[44,915,188],{"class":50},[44,917,918,920,922,924,927],{"class":46,"line":93},[44,919,581],{"class":50},[44,921,584],{"class":58},[44,923,384],{"class":50},[44,925,926],{"class":79},"'추가할 상품:'",[44,928,929],{"class":50},", productId)\n",[44,931,932],{"class":46,"line":100},[44,933,212],{"class":50},[44,935,936,938,940],{"class":46,"line":110},[44,937,86],{"class":50},[44,939,55],{"class":54},[44,941,62],{"class":50},[44,943,944],{"class":46,"line":130},[44,945,97],{"emptyLinePlaceholder":96},[44,947,948,950,952],{"class":46,"line":145},[44,949,51],{"class":50},[44,951,105],{"class":54},[44,953,62],{"class":50},[44,955,956,958],{"class":46,"line":155},[44,957,113],{"class":50},[44,959,960],{"class":54},"ProductCard\n",[44,962,963,966,968,970,972,974],{"class":46,"line":164},[44,964,965],{"class":50},"    :",[44,967,712],{"class":58},[44,969,122],{"class":50},[44,971,717],{"class":79},[44,973,720],{"class":50},[44,975,976],{"class":79},"\"\n",[44,978,979,982,985,987,989,992],{"class":46,"line":169},[44,980,981],{"class":50},"    @",[44,983,984],{"class":58},"add-to-cart",[44,986,122],{"class":50},[44,988,717],{"class":79},[44,990,991],{"class":50},"addToCart",[44,993,976],{"class":79},[44,995,996],{"class":46,"line":182},[44,997,998],{"class":50},"  \u002F>\n",[44,1000,1001,1003,1005],{"class":46,"line":191},[44,1002,86],{"class":50},[44,1004,105],{"class":54},[44,1006,62],{"class":50},[15,1008,1009],{},"데이터 흐름을 한 방향으로 유지하면 상태를 누가 소유하고 변경하는지 추적하기 쉽다.",[19,1011,1014],{"className":1012,"code":1013,"language":24,"meta":25},[22],"부모 상태 ──props──> 자식 화면\n부모 함수 \u003C──emit── 자식 행동\n",[27,1015,1013],{"__ignoreMap":25},[479,1017,1019],{"id":1018},"slot-구조는-자식이-콘텐츠는-부모가-정한다","slot: 구조는 자식이, 콘텐츠는 부모가 정한다",[15,1021,1022],{},[1023,1024,1025],"del",{},"자 재탕해보자, 어머니의 사골국이 떠오른다. 연속으로 12일동안 먹어봤다",[15,1027,1028],{},"카드의 테두리와 간격은 같지만 내부 콘텐츠가 달라진다면,",[15,1030,1031,1034],{},[303,1032,1033],{},"slot이 적합하다",".",[19,1036,1038],{"className":38,"code":1037,"language":40,"meta":25,"style":25},"\u003C!-- BaseCard.vue -->\n\u003Ctemplate>\n  \u003Carticle class=\"card\">\n    \u003Cheader>\u003Cslot name=\"header\" \u002F>\u003C\u002Fheader>\n    \u003Cdiv>\u003Cslot \u002F>\u003C\u002Fdiv>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n",[27,1039,1040,1045,1053,1068,1099,1118,1126],{"__ignoreMap":25},[44,1041,1042],{"class":46,"line":47},[44,1043,1044],{"class":501},"\u003C!-- BaseCard.vue -->\n",[44,1046,1047,1049,1051],{"class":46,"line":65},[44,1048,51],{"class":50},[44,1050,105],{"class":54},[44,1052,62],{"class":50},[44,1054,1055,1057,1059,1061,1063,1066],{"class":46,"line":83},[44,1056,113],{"class":50},[44,1058,619],{"class":54},[44,1060,119],{"class":58},[44,1062,122],{"class":50},[44,1064,1065],{"class":79},"\"card\"",[44,1067,62],{"class":50},[44,1069,1070,1072,1075,1078,1080,1083,1085,1088,1092,1095,1097],{"class":46,"line":93},[44,1071,133],{"class":50},[44,1073,1074],{"class":54},"header",[44,1076,1077],{"class":50},">\u003C",[44,1079,471],{"class":54},[44,1081,1082],{"class":58}," name",[44,1084,122],{"class":50},[44,1086,1087],{"class":79},"\"header\"",[44,1089,1091],{"class":1090},"s7hpK"," \u002F",[44,1093,1094],{"class":50},">\u003C\u002F",[44,1096,1074],{"class":54},[44,1098,62],{"class":50},[44,1100,1101,1103,1106,1108,1110,1112,1114,1116],{"class":46,"line":100},[44,1102,133],{"class":50},[44,1104,1105],{"class":54},"div",[44,1107,1077],{"class":50},[44,1109,471],{"class":54},[44,1111,1091],{"class":1090},[44,1113,1094],{"class":50},[44,1115,1105],{"class":54},[44,1117,62],{"class":50},[44,1119,1120,1122,1124],{"class":46,"line":110},[44,1121,148],{"class":50},[44,1123,619],{"class":54},[44,1125,62],{"class":50},[44,1127,1128,1130,1132],{"class":46,"line":130},[44,1129,86],{"class":50},[44,1131,105],{"class":54},[44,1133,62],{"class":50},[19,1135,1137],{"className":38,"code":1136,"language":40,"meta":25,"style":25},"\u003C!-- 부모 컴포넌트 -->\n\u003CBaseCard>\n  \u003Ctemplate #header>\n    \u003Ch2>서울 날씨\u003C\u002Fh2>\n  \u003C\u002Ftemplate>\n\n  \u003Cp>현재 기온은 28도입니다.\u003C\u002Fp>\n\u003C\u002FBaseCard>\n",[27,1138,1139,1143,1152,1157,1162,1167,1171,1176],{"__ignoreMap":25},[44,1140,1141],{"class":46,"line":47},[44,1142,700],{"class":501},[44,1144,1145,1147,1150],{"class":46,"line":65},[44,1146,51],{"class":50},[44,1148,1149],{"class":54},"BaseCard",[44,1151,62],{"class":50},[44,1153,1154],{"class":46,"line":83},[44,1155,1156],{"class":50},"  \u003Ctemplate #header>\n",[44,1158,1159],{"class":46,"line":93},[44,1160,1161],{"class":50},"    \u003Ch2>서울 날씨\u003C\u002Fh2>\n",[44,1163,1164],{"class":46,"line":100},[44,1165,1166],{"class":50},"  \u003C\u002Ftemplate>\n",[44,1168,1169],{"class":46,"line":110},[44,1170,97],{"emptyLinePlaceholder":96},[44,1172,1173],{"class":46,"line":130},[44,1174,1175],{"class":50},"  \u003Cp>현재 기온은 28도입니다.\u003C\u002Fp>\n",[44,1177,1178,1180,1182],{"class":46,"line":145},[44,1179,86],{"class":50},[44,1181,1149],{"class":54},[44,1183,62],{"class":50},[10,1185,1187],{"id":1186},"상태는-어디에-둘까","상태는 어디에 둘까?",[15,1189,1190,1191],{},"가장 중요한 기준은 ",[303,1192,1193],{},"상태를 실제로 필요한 범위 중 가장 작은 곳에 두는 것",[417,1195,1196,1206],{},[420,1197,1198],{},[423,1199,1200,1203],{},[426,1201,1202],{},"상황",[426,1204,1205],{},"우선 선택",[436,1207,1208,1222,1236,1244,1257],{},[423,1209,1210,1213],{},[441,1211,1212],{},"한 컴포넌트만 사용하는 상태",[441,1214,1215,1218,1219],{},[27,1216,1217],{},"ref",", ",[27,1220,1221],{},"reactive",[423,1223,1224,1227],{},[441,1225,1226],{},"부모와 자식이 함께 사용하는 상태",[441,1228,1229,1230,1232,1233,1235],{},"부모가 소유하고 ",[27,1231,445],{}," \u002F ",[27,1234,458],{}," 사용",[423,1237,1238,1241],{},[441,1239,1240],{},"여러 컴포넌트에서 같은 로직을 재사용",[441,1242,1243],{},"composable",[423,1245,1246,1249],{},[441,1247,1248],{},"가까운 하위 트리에 공통 의존성을 제공",[441,1250,1251,1232,1254],{},[27,1252,1253],{},"provide",[27,1255,1256],{},"inject",[423,1258,1259,1262],{},[441,1260,1261],{},"멀리 떨어진 화면이 같은 상태를 공유",[441,1263,1264],{},"Pinia",[15,1266,1267],{},"드롭다운 열림 여부, 입력 중인 값, hover 상태처럼 해당 컴포넌트가 사라질 때 함께 없어져도 되는 값은 전역 저장소에 둘 이유가 없다.",[19,1269,1271],{"className":312,"code":1270,"language":314,"meta":25,"style":25},"const isDropdownOpen = ref(false)\nconst inputValue = ref('')\n",[27,1272,1273,1292],{"__ignoreMap":25},[44,1274,1275,1277,1280,1282,1285,1287,1290],{"class":46,"line":47},[44,1276,69],{"class":68},[44,1278,1279],{"class":72}," isDropdownOpen",[44,1281,76],{"class":68},[44,1283,1284],{"class":58}," ref",[44,1286,384],{"class":50},[44,1288,1289],{"class":72},"false",[44,1291,405],{"class":50},[44,1293,1294,1296,1299,1301,1303,1305,1308],{"class":46,"line":65},[44,1295,69],{"class":68},[44,1297,1298],{"class":72}," inputValue",[44,1300,76],{"class":68},[44,1302,1284],{"class":58},[44,1304,384],{"class":50},[44,1306,1307],{"class":79},"''",[44,1309,405],{"class":50},[15,1311,1312],{},"반대로 로그인 사용자나 장바구니처럼 서로 먼 화면이 함께 읽고 바꾸는 상태는 Pinia 같은 저장소가 유용하다.",[10,1314,1316],{"id":1315},"composable로-로직-재사용하기","composable로 로직 재사용하기",[15,1318,1319],{},"UI가 아니라 상태와 동작이 반복된다면 컴포넌트를 복사하지 말고 composable로 분리",[19,1321,1323],{"className":312,"code":1322,"language":314,"meta":25,"style":25},"\u002F\u002F composables\u002FuseToggle.js\nimport { ref } from 'vue'\n\nexport function useToggle(initialValue = false) {\n  const value = ref(initialValue)\n\n  const toggle = () => {\n    value.value = !value.value\n  }\n\n  return { value, toggle }\n}\n",[27,1324,1325,1330,1341,1345,1369,1384,1388,1403,1416,1420,1424,1432],{"__ignoreMap":25},[44,1326,1327],{"class":46,"line":47},[44,1328,1329],{"class":501},"\u002F\u002F composables\u002FuseToggle.js\n",[44,1331,1332,1334,1337,1339],{"class":46,"line":65},[44,1333,251],{"class":68},[44,1335,1336],{"class":50}," { ref } ",[44,1338,257],{"class":68},[44,1340,328],{"class":79},[44,1342,1343],{"class":46,"line":83},[44,1344,97],{"emptyLinePlaceholder":96},[44,1346,1347,1350,1353,1356,1358,1361,1363,1366],{"class":46,"line":93},[44,1348,1349],{"class":68},"export",[44,1351,1352],{"class":68}," function",[44,1354,1355],{"class":58}," useToggle",[44,1357,384],{"class":50},[44,1359,1360],{"class":907},"initialValue",[44,1362,76],{"class":68},[44,1364,1365],{"class":72}," false",[44,1367,1368],{"class":50},") {\n",[44,1370,1371,1374,1377,1379,1381],{"class":46,"line":100},[44,1372,1373],{"class":68},"  const",[44,1375,1376],{"class":72}," value",[44,1378,76],{"class":68},[44,1380,1284],{"class":58},[44,1382,1383],{"class":50},"(initialValue)\n",[44,1385,1386],{"class":46,"line":110},[44,1387,97],{"emptyLinePlaceholder":96},[44,1389,1390,1392,1395,1397,1399,1401],{"class":46,"line":130},[44,1391,1373],{"class":68},[44,1393,1394],{"class":58}," toggle",[44,1396,76],{"class":68},[44,1398,571],{"class":50},[44,1400,574],{"class":68},[44,1402,188],{"class":50},[44,1404,1405,1408,1410,1413],{"class":46,"line":145},[44,1406,1407],{"class":50},"    value.value ",[44,1409,122],{"class":68},[44,1411,1412],{"class":68}," !",[44,1414,1415],{"class":50},"value.value\n",[44,1417,1418],{"class":46,"line":155},[44,1419,550],{"class":50},[44,1421,1422],{"class":46,"line":164},[44,1423,97],{"emptyLinePlaceholder":96},[44,1425,1426,1429],{"class":46,"line":169},[44,1427,1428],{"class":68},"  return",[44,1430,1431],{"class":50}," { value, toggle }\n",[44,1433,1434],{"class":46,"line":182},[44,1435,212],{"class":50},[19,1437,1439],{"className":38,"code":1438,"language":40,"meta":25,"style":25},"\u003Cscript setup>\nimport { useToggle } from '.\u002Fcomposables\u002FuseToggle'\n\nconst { value: isOpen, toggle } = useToggle()\n\u003C\u002Fscript>\n",[27,1440,1441,1451,1463,1467,1497],{"__ignoreMap":25},[44,1442,1443,1445,1447,1449],{"class":46,"line":47},[44,1444,51],{"class":50},[44,1446,55],{"class":54},[44,1448,59],{"class":58},[44,1450,62],{"class":50},[44,1452,1453,1455,1458,1460],{"class":46,"line":65},[44,1454,251],{"class":68},[44,1456,1457],{"class":50}," { useToggle } ",[44,1459,257],{"class":68},[44,1461,1462],{"class":79}," '.\u002Fcomposables\u002FuseToggle'\n",[44,1464,1465],{"class":46,"line":83},[44,1466,97],{"emptyLinePlaceholder":96},[44,1468,1469,1471,1474,1477,1479,1482,1484,1487,1490,1492,1494],{"class":46,"line":93},[44,1470,69],{"class":68},[44,1472,1473],{"class":50}," { ",[44,1475,1476],{"class":907},"value",[44,1478,197],{"class":50},[44,1480,1481],{"class":72},"isOpen",[44,1483,1218],{"class":50},[44,1485,1486],{"class":72},"toggle",[44,1488,1489],{"class":50}," } ",[44,1491,122],{"class":68},[44,1493,1355],{"class":58},[44,1495,1496],{"class":50},"()\n",[44,1498,1499,1501,1503],{"class":46,"line":100},[44,1500,86],{"class":50},[44,1502,55],{"class":54},[44,1504,62],{"class":50},[10,1506,1508],{"id":1507},"컴포넌트-생명주기","컴포넌트 생명주기",[15,1510,1511],{},"컴포넌트는 설정되고, DOM에 연결되고, 상태 변경에 따라 갱신된 뒤 제거된다.",[417,1513,1514,1527],{},[420,1515,1516],{},[423,1517,1518,1521,1524],{},[426,1519,1520],{},"단계",[426,1522,1523],{},"대표 훅",[426,1525,1526],{},"자주 하는 작업",[436,1528,1529,1545,1561],{},[423,1530,1531,1534,1542],{},[441,1532,1533],{},"마운트 전후",[441,1535,1536,1218,1539],{},[27,1537,1538],{},"onBeforeMount",[27,1540,1541],{},"onMounted",[441,1543,1544],{},"DOM 접근, 브라우저 API 연결",[423,1546,1547,1550,1558],{},[441,1548,1549],{},"업데이트 전후",[441,1551,1552,1218,1555],{},[27,1553,1554],{},"onBeforeUpdate",[27,1556,1557],{},"onUpdated",[441,1559,1560],{},"갱신된 DOM 측정",[423,1562,1563,1566,1574],{},[441,1564,1565],{},"언마운트 전후",[441,1567,1568,1218,1571],{},[27,1569,1570],{},"onBeforeUnmount",[27,1572,1573],{},"onUnmounted",[441,1575,1576],{},"타이머와 이벤트 리스너 정리",[19,1578,1580],{"className":38,"code":1579,"language":40,"meta":25,"style":25},"\u003Cscript setup>\nimport { onMounted, onUnmounted } from 'vue'\n\nconst handleResize = () => {\n  console.log(window.innerWidth)\n}\n\nonMounted(() => {\n  window.addEventListener('resize', handleResize)\n})\n\nonUnmounted(() => {\n  window.removeEventListener('resize', handleResize)\n})\n\u003C\u002Fscript>\n",[27,1581,1582,1592,1603,1607,1622,1631,1635,1639,1650,1666,1670,1674,1684,1697,1701],{"__ignoreMap":25},[44,1583,1584,1586,1588,1590],{"class":46,"line":47},[44,1585,51],{"class":50},[44,1587,55],{"class":54},[44,1589,59],{"class":58},[44,1591,62],{"class":50},[44,1593,1594,1596,1599,1601],{"class":46,"line":65},[44,1595,251],{"class":68},[44,1597,1598],{"class":50}," { onMounted, onUnmounted } ",[44,1600,257],{"class":68},[44,1602,328],{"class":79},[44,1604,1605],{"class":46,"line":83},[44,1606,97],{"emptyLinePlaceholder":96},[44,1608,1609,1611,1614,1616,1618,1620],{"class":46,"line":93},[44,1610,69],{"class":68},[44,1612,1613],{"class":58}," handleResize",[44,1615,76],{"class":68},[44,1617,571],{"class":50},[44,1619,574],{"class":68},[44,1621,188],{"class":50},[44,1623,1624,1626,1628],{"class":46,"line":100},[44,1625,581],{"class":50},[44,1627,584],{"class":58},[44,1629,1630],{"class":50},"(window.innerWidth)\n",[44,1632,1633],{"class":46,"line":110},[44,1634,212],{"class":50},[44,1636,1637],{"class":46,"line":130},[44,1638,97],{"emptyLinePlaceholder":96},[44,1640,1641,1643,1646,1648],{"class":46,"line":145},[44,1642,1541],{"class":58},[44,1644,1645],{"class":50},"(() ",[44,1647,574],{"class":68},[44,1649,188],{"class":50},[44,1651,1652,1655,1658,1660,1663],{"class":46,"line":155},[44,1653,1654],{"class":50},"  window.",[44,1656,1657],{"class":58},"addEventListener",[44,1659,384],{"class":50},[44,1661,1662],{"class":79},"'resize'",[44,1664,1665],{"class":50},", handleResize)\n",[44,1667,1668],{"class":46,"line":164},[44,1669,555],{"class":50},[44,1671,1672],{"class":46,"line":169},[44,1673,97],{"emptyLinePlaceholder":96},[44,1675,1676,1678,1680,1682],{"class":46,"line":182},[44,1677,1573],{"class":58},[44,1679,1645],{"class":50},[44,1681,574],{"class":68},[44,1683,188],{"class":50},[44,1685,1686,1688,1691,1693,1695],{"class":46,"line":191},[44,1687,1654],{"class":50},[44,1689,1690],{"class":58},"removeEventListener",[44,1692,384],{"class":50},[44,1694,1662],{"class":79},[44,1696,1665],{"class":50},[44,1698,1699],{"class":46,"line":209},[44,1700,555],{"class":50},[44,1702,1703,1705,1707],{"class":46,"line":215},[44,1704,86],{"class":50},[44,1706,55],{"class":54},[44,1708,62],{"class":50},[15,1710,1711,1714,1715,1718,1719,1722,1723,1726],{},[27,1712,1713],{},"onMounted()","에서 등록한 외부 이벤트와 타이머는 ",[27,1716,1717],{},"onUnmounted()","에서 해제한다. 단순한 파생 값은 ",[27,1720,1721],{},"onUpdated()","보다 ",[27,1724,1725],{},"computed()","로 표현하는 편이 명확하다.",[10,1728,1730],{"id":1729},"설계-점검표","설계 점검표",[15,1732,1733],{},"컴포넌트를 나눌 때 아래 질문을 확인하자",[1735,1736,1737,1741,1744,1747,1750,1753],"ol",{},[1738,1739,1740],"li",{},"이 컴포넌트의 책임을 한 문장으로 설명할 수 있는가?",[1738,1742,1743],{},"입력은 props, 출력은 emit으로 드러나는가?",[1738,1745,1746],{},"props를 자식에서 직접 수정하고 있지는 않은가?",[1738,1748,1749],{},"반복되는 UI는 컴포넌트, 반복되는 로직은 composable로 분리했는가?",[1738,1751,1752],{},"지역 상태를 불필요하게 Pinia로 올리지 않았는가?",[1738,1754,1755],{},"외부 이벤트와 타이머를 언마운트 시점에 정리하는가?",[15,1757,1758],{},"컴포넌트 설계의 목표는 파일 수를 늘리는 것이 아니다. 상태의 소유자와 컴포넌트 사이의 계약을 분명하게 만드는 것이다.",[10,1760,1762],{"id":1761},"참고-자료","참고 자료",[1764,1765,1766,1775,1782,1789],"ul",{},[1738,1767,1768],{},[1769,1770,1774],"a",{"href":1771,"rel":1772},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fessentials\u002Fcomponent-basics.html",[1773],"nofollow","Vue 공식 문서: Components Basics",[1738,1776,1777],{},[1769,1778,1781],{"href":1779,"rel":1780},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fcomponents\u002Fevents.html",[1773],"Vue 공식 문서: Component Events",[1738,1783,1784],{},[1769,1785,1788],{"href":1786,"rel":1787},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fcomponents\u002Fslots.html",[1773],"Vue 공식 문서: Slots",[1738,1790,1791],{},[1769,1792,1795],{"href":1793,"rel":1794},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fessentials\u002Flifecycle.html",[1773],"Vue 공식 문서: Lifecycle Hooks",[1797,1798],"hr",{},[174,1800,1801],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":25,"searchDepth":65,"depth":65,"links":1803},[1804,1805,1806,1811,1812,1813,1814,1815],{"id":12,"depth":65,"text":13},{"id":224,"depth":65,"text":225},{"id":411,"depth":65,"text":412,"children":1807},[1808,1809,1810],{"id":481,"depth":83,"text":482},{"id":740,"depth":83,"text":741},{"id":1018,"depth":83,"text":1019},{"id":1186,"depth":65,"text":1187},{"id":1315,"depth":65,"text":1316},{"id":1507,"depth":65,"text":1508},{"id":1729,"depth":65,"text":1730},{"id":1761,"depth":65,"text":1762},[1817,1818],"Frontend","Vue","2026-08-03 01:00:00 +0900","Vue 컴포넌트의 구조와 데이터 흐름, 상태 위치를 결정하는 기준, 생명주기 훅을 예제로 정리.","md",null,[1824,1825,1826],"부모는 props로 데이터를 전달하고 자식은 emit으로 변경 의도를 알림.","slot은 공통 구조를 유지하면서 내부 콘텐츠를 교체.","상태는 필요한 컴포넌트 범위 중 가장 작은 곳에 둠.","\u002Ffrontend\u002Fvue\u002F2026\u002F08\u002F03\u002Fvue-component\u002F",{"layout":1829,"excerpt_separator":1830,"excerpt":1831},"post","\u003C!--more-->",{"type":7,"value":1832},[1833,1835,1837,1842,1846],[10,1834,13],{"id":12},[15,1836,17],{},[19,1838,1840],{"className":1839,"code":23,"language":24,"meta":25},[22],[27,1841,23],{"__ignoreMap":25},[15,1843,31,1844,35],{},[27,1845,34],{},[174,1847,1801],{},"\u002Fposts\u002Ffrontend\u002Fvue\u002F2026-08-03-vue-component",{"title":5,"description":1820},"posts\u002FFrontend\u002FVue\u002F2026-08-03-vue-component","Vue 컴포넌트의 입력과 출력, 콘텐츠 확장, 상태 공유 범위와 생명주기 정리 방법을 살펴본다.",[1817,1853,1854,1855,1856,1857,1858],"Vue.js","Component","Props","Emits","Slots","Lifecycle","umx665sSKkov1B8l2YqtwxZd_A_YwTDZnT6tJGSc9cM",1788744789873]