cover

하나의 컴포넌트로 여러 개의 동일한 인스턴스를 만들 수 있지만 각 인스턴스마다 조금 다른 기능이나 내용이 들어가게 하고 싶을 수 있습니다. 블로그로 보자면 글 목록이 하나의 예인데요. 레이아웃은 동일한데 제목과 설명글만 달라지게 해야합니다. 이를 위해 사용하는 것이 props인데요. 어떻게 사용하는 지 자세히 알아보겠습니다.


props란?

propproperty의 줄임말로, 컴포넌트에서 사용할 속성을 추가할 수 있습니다.

html
      <BlogPost post-title="글 제목" desc="설명 설명" />
    

이렇게 속성을 추가하면 어떤 장점이 있는 걸까요? 바로 외부에서 컴포넌트에 데이터를 전달할 수 있게 됩니다. 하지만 아무 속성이나 모두 받는 것은 아닙니다. 해당 컴포넌트에서 받을 데이터들을 정의해주어야합니다.

컴포넌트에서 props 정의

props라는 속성을 등록할 수 있습니다. 이 때 배열 또는 객체로 전달할 수 있습니다.

props 배열로 정의

문자열의 배열로 속성을 정의할 수 있습니다.

vue
BlogPost.vue
      export default {
  props: ['postTitle', 'desc']
}
    
info
      보통 속성을 넣을 땐 중간에 하이픈(-)이 들어간 kebab-case을 사용하고, props를 정의하고 이를 사용할 때에는 중간에 대문자가 들어간 camelCase를 씁니다. HTML에서 쓰이냐, Javascript에서 쓰이냐의 차이 때문이죠. 컴포넌트의 경우는 가독성을 위해 중간과 첫 글자가 모두 대문자인 PascalCase를 사용합니다.
    

props 객체로 정의

객체는 다음과 같이 정의할 수 있는데 배열과 달리, 타입이나 조건을 통해 제한을 둘 수 있습니다.

vue
BlogPost.vue
      export default {
	props: {
		postTitle: {
			type: String, // 문자열로 제한
			required: true // 필수로 넣어야함
		},
		desc: {
			type: String, // 문자열로 제한
			default: "" // 기본값 ""
		},
	}
}
    

대략 이 정도만 알아두고 자세한 건 아래에서 다루겠습니다.

Props 속성 전달하기

props를 정의했다면 이제 컴포넌트를 사용할 때 속성으로 전달하면 됩니다.

html
      <BlogPost post-title="글 제목" desc="설명 설명" />
    

v-bind로 전달

문자열로 전달해도 되지만, v-bind도 사용 가능합니다.

html
      <template>
	<BlogPost :post-title="title" :desc="desc" />
</template>

<script>
export default {
	data() {
		return {
			title: "글 제목",
			desc: "설명 설명"
		};
	}
}
</script>
    

v-for + v-bind로 전달하기

맨 처음 예시를 들었던 블로그 글 목록을 만드려면 v-for와 같은 반복문이 필요하게 됩니다.

html
      <template>
	<BlogPost 
		v-for="post of posts"
		:post-title="post.title" 
		:desc="post.desc"
		:key="post.title"
		/>
</template>

<script>
export default {
	data() {
		return {
			posts: [
				{
					title: "햄이네 박사라네",
					desc: "아이네의 모시꺵이 같은 겁니다."
				},
				// ...
			]
		};
	}
}
</script>
    

props 사용하기

속성으로 전달된 props는 컴포넌트의 데이터(data)처럼 사용할 수 있습니다.

html
      <template>
	<div class="post">
		<h1> {{ postTitle }} </h1>
		<p> {{ desc }} </p>
	</div>
</template>
    

주의할 점은 데이터와 달리 외부에서 가져온 값이기 때문에 읽기 전용이라는 점입니다. 부모 컴포넌트에서 값을 바꾸면 자동으로 props에 전달된 값도 변경되지만 반대는 적용되지 않습니다. 그래서 값을 덮어쓰려고 한다면,

javascript
      export default {
  props: ["postTitle", "desc"],
  mounted() {
		// ❌ props 값은 새로 쓸 수 없음!
    this.postTitle += "글쓴이 : vuelogger";
  },
};
    

다음과 같은 에러를 만납니다.

loading

만약 자식 컴포넌트에서 부모 컴포넌트의 데이터를 바꿀 수 있다면 상위 컴포넌트 데이터가 바뀌었을 때, 그것이 어디에서 바뀌었는지 찾기 어렵기 때문에 읽기 전용으로 한것입니다.

props 값을 가져와서 바꾸고 싶다면?

값을 바꾸어야하는 상황은 여러 가지가 있을 수 있습니다. 예를 들면 counter 초기값을 받는 용도가 될 수 있겠죠. 그러면 초기값을 받아 data에서 새로 만들면 됩니다.

javascript
      export default {
  props: ['initialCounter'],
  data() {
    return {
      counter: this.initialCounter
    }
  }
}
    
warn
      이후 initialCounter 값이 바뀌더라도, 이미 counter 값은 생성되었으니 바뀌지 않습니다.
    

아니면 값을 가져와서 조금만 변경하고 싶을 수도 있습니다. 문자열을 받는데 공백을 제거한다거나 소문자로 바꾸는 등 말이죠.

javascript
      export default {
  props: ['size'],
  computed: {
    // size 값이 바뀔 때마다 업데이트 됩니다.
    normalizedSize() {
      return this.size.trim().toLowerCase()
    }
  }
}
    

만약 props로 배열이나 객체를 전달했다면 내부 값을 바꿀 수는 있습니다. 왜냐하면 배열이나 객체 자체는 변경할 수 없지만 내부 값은 참조하기 때문이죠.

html
      <template>
	<!-- 이 버튼을 누르면 나이가 점점 늘어나요!! -->
  <button @click="user.age++">{{ user.age }}</button>
</template>

<script>
export default {
  props: ["user"],
};
</script>
    

하지만 이 방법은 추천하지 않습니다. 위에서도 말씀드렸듯이 문제가 발생했을 때 원인을 찾기 어렵게 하는 요인이기 때문입니다. 자식 컴포넌트에서 변경이 필요한 일이 발생한다면 이를 부모에 전달해서 부모 쪽에서 값을 변경하도록 해야합니다. 이는 이벤트 전달(emit an event)을 통해 할 수 있는데 추후 다뤄보겠습니다.

props 값 제한하기

위에서 잠시 객체로 props를 선언해서 타입이나 필수인지 여부 등의 제한 사항을 두는 방법이 있다는 것을 알았습니다. 그 외에도 몇 가지가 더 가능한데요. 예시들을 보겠습니다.

javascript
      export default {
  props: {
    // 타입 제한
    propA: Number,
		// 여러개 타입 제한
    propB: [String, Number],
		// type 키로 타입 제한
    // 필수로 넣어줘야함을 명시
    propC: {
			type: String,
      required: true // 기본적으로는 모든 prop은 선택사항
    },
    // 기본값 (default)
		// 외부에서 값을 정해주지 않은 경우나 undefined인 경우
		// default에 명시한 값을 사용
    propD: {
      type: Number,
      default: 100 // 기본값이 없는 경우 undefined
									 // Boolean의 경우 false
    },
    // 객체나 배열 타입의 경우는 함수를 통해 반환해야함
    propE: {
      type: Object,
			// default 함수에서 매개변수로 들어온 값을 가져올 수 있음
      default(rawProps) {
        return { message: 'hello' }
      }
    },
		// 함수 타입인 경우 default에 넣어준 함수가 곧 default 함수가 됨
    propG: {
      type: Function,
      // Default function이라는 글자를 반환하는 함수가 디폴트가 됨
      default() {
        return 'Default function'
      }
    }
    // 커스텀으로 제한
    propF: {
      validator(value) {
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
  }
}
    

Lifecycle hook을 보면 prop이 data나 computed 등 보다 먼저 생성되기 때문에 default, validator 함수에서 컴포넌트의 데이터나 함수들을 사용할 수 없습니다.

커스텀 타입 검사

기본적인 타입은 다음과 같습니다.

String
Number
Boolean
Array
Object
Date
Function
Symbol

그런데 만약 어떤 클래스를 만들었고 이 클래스가 들어왔는지 검사하고 싶다면 이 또한 가능합니다. Person이라는 클래스를 만들었다고 할 때

javascript
      class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}
    

Person 클래스를 타입 넣듯이 넣어주면 됩니다.

javascript
      export default {
  props: {
    author: Person
  }
}
    

외부에서 들어온 author라는 값이 Person의 인스턴스인지 instanceof Person로 확인할 겁니다.

값이 필요없는 Boolean 속성

속성들 중에는 disabled, checked 와 같이 값을 넣어줄 필요없는 것들도 있습니다. prop도 마찬가지로 Boolean 타입으로 정의했다면, 외부에서 사용할 때도 동일하게 해주면 됩니다.

javascript
      export default {
  props: {
    disabled: Boolean
  }
}
    

이 컴포넌트에 true 또는 false 값을 전달할 수도 있지만, 속성만 적거나 아무것도 안 적는 것도 동일하게 적용됩니다.

html
      <!-- :disabled="true" 와 동일 -->
<MyComponent disabled />

<!-- :disabled="false" 와 동일 -->
<MyComponent />