프론트 팀원들에게 배운 Front 유지보수 기법(vue3 composition API 기준)

허재원

Updated:

프론트 팀원들에게 배운 Front 유지보수 기법(vue3 composition API 기준)

프론트 팀원들에게 배운 것 중 유지보수 관점으로 묶을 수 있는 것을 정리하였습니다. 여전히 부족한 점이 많으니 참고하여 읽어주시면 감사하겠습니다.



1. axios api를 관리하는 파일 만들기


  • axios를 사용할 때, 일일이 각 method에서 직접 호출하는 식으로 사용했습니다.
async function onFormSubmit() {
  const formData = new FormData();
  formData.append("boardTitle", boardTitle.value);
  formData.append("boardContent", boardContent.value);
  formData.append("boardWriter", boardWriter.value);

  await axios.post("/api/boards", formData);
}
  • 위의 방식으로는 파일이 많아질수록 관리가 어렵기 때문에 따로 api를 모아놓는 파일을 만들어야 합니다.
  • backend와 연동할 때 대부분 /api가 붙기 때문에 baseURL도 api를 붙인 상태로 설정했습니다.
//instance.ts
export const instance = axios.create({
  timeout: 10 * 1000,
  baseURL: "http://localhost:8080/api",
});

//api.ts
export const boardInsert = (formData: FormData) => {
  instance.post("/boards", formData);
};

//*.vue
async function onFormSubmit() {
  const formData = new FormData();
  formData.append("boardTitle", boardTitle.value);
  formData.append("boardContent", boardContent.value);
  formData.append("boardWriter", boardWriter.value);
  await boardInsert(formData).then((response) => {
    alert("등록이 완료되었습니다.");
  });
}
  • instance에서 url과 method 등을 구분해서 return하면 가독성이 더 좋아집니다.
export const boardInsert = (formData: FormData) => {
  return instance({
    url: "/boards",
    method: "post",
    data: formData,
  });
};


2. 사용자 정의 interface 활용하기


  • 위에서는 axios의 parameter type으로 FormData interface를 사용했습니다.
  • 그보다 자신이 사용할 property를 지정하는 custom interface를 만들어서 활용하면 내용이 더 명확해집니다.
  • 아래와 같이 새로운 interface와 class를 만들었습니다. interface를 지정했다면 class는 빈 생성자로 놔둬도 알아서 해당 요소를 갖는 class를 생성하게 한다고 합니다.
export class Board {}
export interface Board {
  boardNo: number;
  boardTitle: string;
  boardContent: string;
  boardWriter: string;
  boardView: number;
  boardRegisterDate: Date;
}
  • 이제는 FormData class를 활용하지 않고 직접 json object를 만들어 보내면 됩니다.
async function onFormSubmit() {
  const boardInfo = {
    boardNo,
    boardTitle: boardTitle.value,
    boardContent: boardContent.value,
    boardWriter: boardWriter.value,
    boardView,
    boardRegisterDate,
  };
  await boardInsert(boardInfo).then((response) => {
    alert("등록이 완료되었습니다.");
  });
};

export const boardInsert = (formData: Board) =>{
    .
    .
    .
};
  • 하지만 게시판을 등록할 때 등록날짜가 먼저 있을 수는 없습니다. 게시글 등록날짜는 게시판에 관한 정보를 response로 받아올 때 필요한 field입니다.
  • 다시말해 request로 보내는 data와 response로 받는 data가 다르기에 따로 구분해서 만들어줍니다.
//board.model.ts
export class BoardRequest {}
export interface BoardRequest {
  boardTitle: string;
  boardContent: string;
  boardWriter: string;
}

export class BoardResponse {}
export interface BoardResponse {
  boardNo: number;
  boardTitle: string;
  boardContent: string;
  boardWriter: string;
  boardView: number;
  boardRegisterDate: Date;
  boardPhoto: string;
}
  • 그런데 게시판을 등록할 때 혹시 누군가는 내용을 넣고 싶지 않을수도 있습니다.
  • 이와 같이 생략가능(optional) 속성은 ?를 통해 부여할 수 있습니다. optional이 되면 좀 더 유연하게 활용할 수 있습니다.
export class BoardRequest {}
export interface BoardRequest {
  boardTitle: string;
  boardContent?: string;
  boardWriter: string;
}
  • 다만 optional의 경우 값이 없거나 할당되지 않은 상태일 수 있습니다. 따라서 값이 있는지 확인하는 절차가 필요합니다.
  • undefined나 null의 경우 falsy 값이기 때문에 조건문이 false가 되고 if문 안으로 들어가지 않게 됩니다.
  • 이같이 if문을 통해서 적절한 값이 존재하는지 확인한 뒤 대입 처리를 해줄 수 있습니다.
  • 그 외에도 optional chaining(?.), null coalescing(??) 등의 방법도 유효할 수 있습니다.
if (boardContent) {
  boardContent.value = "value가 존재합니다";
}
  • custom interface가 완성되었습니다.
  • axios api가 받는 parameter의 type 또한 추상적인 formData interface가 아닌 구체적인 특성을 지닌 interface를 명시했습니다.
  • boardContent의 경우 optional이라서 여기서는 optional chaining을 활용하였습니다.
  • optional chaining은 optional property에 대해서만 적용해야 합니다.
async function onFormSubmit() {
  const boardInfo = {
    boardTitle: boardTitle.value,
    boardContent: boardContent?.value,
    boardWriter: boardWriter.value,
  };

  await boardInsert(boardInfo).then((response) => {
    alert("등록이 완료되었습니다.");
  });
}

//api.ts
export const boardInsert = (boardInfo: Board) => {
  return instance({
    url: "/boards",
    method: "post",
    data: boardInfo,
  });
};
  • 가독성을 해치는 then문을 없애주면 더 알아보기 쉬워지게 됩니다.
async function onFormSubmit() {
  const boardInfo = {
    boardTitle: boardTitle.value,
    boardContent: boardContent?.value,
    boardWriter: boardWriter.value,
  };

  const statusCode = (await boardInsert(boardInfo)).status;
  if (statusCode === 200) {
    alert("등록이 완료되었습니다.");
  }
}
  • 함수명도 더 직관적으로 와닿게 지어주고, 함수라는 것을 나타내기 위해 동사가 앞으로 오게 배치합니다.
async function handleFormSave() {
  const boardInfo = {
    boardTitle: boardTitle.value,
    boardContent: boardContent?.value,
    boardWriter: boardWriter.value,
  };

  const statusCode = (await boardInsert(boardInfo)).status;
  if (statusCode === 200) {
    alert("등록이 완료되었습니다.");
  }
}


3. JS 내장 interface 활용하기


  • typescript를 활용하기 이전에는 아래와 같이 간단하게 file 값을 가져올 수 있었습니다.
function onFileChange($event) {
  fileData.value = $event.target.files[0];
}
  • 하지만 typescript에서는 parameter의 type을 반드시 지정해야 합니다. 내장 interface 중 Event를 사용하면 됩니다.
function onFileChange($event: Event) {
  fileData.value = event.target.files[0];
}
  • Event의 target이라는 property는 EventTarget이라는 interface에 속합니다.
  • 그런데 EventTarget에는 files라는 속성이 없습니다. files라는 property는 EventTarget에서 가져오는 게 아닙니다.
  • files는 HTMLInputElement에서 가져오는 속성이라 HTMLInputElement에서 가져왔음을 명시해주어야 합니다.
  • 명시해주는 방법으로 타입 단언이 있습니다. 아래와 같이 as를 붙여서 이것은 확실하게 이러한 type임을 선언해주는 행위입니다.
const target = $event.target as HTMLInputElement;
  fileData.value = target.files[0];
  • 하지만 위와 같이 쓰면 IDE에서 오류를 출력합니다. files 속성은 required가 아니라서 null일 가능성이 있기 때문입니다.
  • 따라서 아래와 같이 값이 존재하는 경우에만 값을 대입하게 if문을 활용합니다.
const target = $event.target as HTMLInputElement;
  if (target && target.files) {
    fileData.value = target.files[0];
  }
  • optional chaining은 optional property에 대해서만 적용해야 한다고 하였습니다.
  • HTMLImageElement의 src가 그 대표 사례입니다. src는 optional한 property가 아닌 required property입니다.
  • 그러한 이유로 아래와 같이 optional chaining으로 값을 받아오려면 IDE에서 The left-hand side of an assignment expression may not be an optional property access라는 오류를 표시합니다.
  • 이 오류는 우리를 괴롭히려는 게 아니라 runtime error를 줄여주기 위함입니다.
  <img :src="'/upload/' + boardInfo.boardPhoto" ref="imagePreview" id="imagePreview"/>
  //이하 script
  const imagePreview = ref<HTMLImageElement>();
  const onFileChange = ($event: Event) => {
.
.
        imagePreview.value?.src = URL.createObjectURL(fileData.value);
      }
      // (document.getElementById('imagePreview') as HTMLImageElement).src = URL.createObjectURL(fileData.value);
  • 따라서 아래와 같이 다시 쓸 수있습니다. optional property가 아닌 경우에는 if문으로 값이 있는 지 확인합니다.
  • 또한 dom 객체를 직접 조작하기보단 vue의 ref 변수를 활용하는 것이 좋습니다.
  <img :src="'/upload/' + boardInfo.boardPhoto" ref="imagePreview" />
  //이하 script
  const imagePreview = ref<HTMLImageElement>();
  const onFileChange = ($event: Event) => {
.
.
      if (imagePreview.value) {
        imagePreview.value.src = URL.createObjectURL(fileData.value);
      }
    };
  • 만약 event가 esc와 같은 keyBoardEvent라면 어떨까요?
function onEscapeKeydown($event) {
  if ($event.key === "Escape") {
    clearSeatItems();
  }
}
  • 그럴 때는 Event interface가 아닌 keyboardEvent interface로 명시해주어야 합니다.
function onEscapeKeydown($event: KeyboardEvent) {
  if ($event.key === "Escape") {
    clearSeatItems();
  }
}


4. component 분리 및 재사용하기


  • 본래는 아래와 같이 기다란 component를 가지고 있었습니다.
    <v-app id="inspire">
      <v-main class="blue-grey lighten-4">
        <v-container class="mt-5" style="max-width: 700px" fill-height>
          <v-card>
            <div class="pa-15">
              <h1 style="text-align: center" class="mb-10">세부조회 화면</h1>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="출발역" :items="['수서', '동탄', '오송', '대전', '신경주', '울산', '부산', '공주', '익산', '목포']" v-model="scheduleInfo.scheduleDepartStation"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="도착역" :items="['수서', '동탄', '오송', '대전', '신경주', '울산', '부산', '공주', '익산', '목포']" v-model="scheduleInfo.scheduleArriveStation"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-text-field label="날짜" v-model="scheduleInfo.scheduleDate"></v-text-field>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="시간" :items="['00시 이후', '04시 이후', '08시 이후', '12시 이후', '16시 이후', '20시 이후']" v-model="scheduleInfo.scheduleTime"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="성인 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountAdult"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="어린이 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountChild"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="노인 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountElder"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="중증 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountSevere"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
            </div>
            <v-btn type="button" color="blue lighten-1 text-capitalize" depressed large block dark class="mb-3" @click="handleFetchSchedule"> 조회하기 </v-btn>
          </v-card>
        </v-container>
      </v-main>

      <v-table>
        <tr>
          <td>출발역</td>
          <td>도착역</td>
          <td>출발시간</td>
          <td>도착시간</td>
          <td>기차번호</td>
        </tr>
        <tr v-for="schedule in scheduleList" :key="schedule.scheduleNo">
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </v-table>
    </v-app>
  • 하지만 길어서 보기 불편합니다. 분리하고 싶습니다.
  • 일정의 필터링 조건과, 필터링 조건에 따른 일정 결과로 분리될 수 있을 거 같습니다.
  • 아래서 테이블은 조건을 결정한 이후에 조회하기를 누르면 일정 결과를 보여주는 component입니다.
  <v-table>
    <tr>
      <td>출발역</td>
      <td>도착역</td>
      <td>출발시간</td>
      <td>도착시간</td>
      <td>기차번호</td>
      <td>특실</td>
      <td>일반실</td>
    </tr>
    <tr v-for="schedule in scheduleList" :key="schedule.scheduleNo">
      <td></td>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </v-table>
  • component는 분리했습니다. 그러나 일정의 필터링 조건 component는 여전히 반복되는 것들이 많습니다.
    <v-app id="inspire">
      <v-main class="blue-grey lighten-4">
        <v-container class="mt-5" style="max-width: 700px" fill-height>
          <v-card>
            <div class="pa-15">
              <h1 style="text-align: center" class="mb-10">세부조회 화면</h1>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="출발역" :items="['수서', '동탄', '오송', '대전', '신경주', '울산', '부산', '공주', '익산', '목포']" v-model="scheduleInfo.scheduleDepartStation"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="도착역" :items="['수서', '동탄', '오송', '대전', '신경주', '울산', '부산', '공주', '익산', '목포']" v-model="scheduleInfo.scheduleArriveStation"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-text-field label="날짜" v-model="scheduleInfo.scheduleDate"></v-text-field>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="시간" :items="['00시 이후', '04시 이후', '08시 이후', '12시 이후', '16시 이후', '20시 이후']" v-model="scheduleInfo.scheduleTime"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="성인 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountAdult"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="어린이 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountChild"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="노인 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountElder"></v-select>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="중증 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountSevere"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
            </div>
            <v-btn type="button" color="blue lighten-1 text-capitalize" depressed large block dark class="mb-3" @click="handleFetchSchedule"> 조회하기 </v-btn>
          </v-card>
        </v-container>
      </v-main>

5. v-for를 활용해 반복되는 template 코드 간소화하기


  • 위의 코드에서 단순 반복을 줄이기 위해 v-for와 배열을 활용합니다.
  • 우선 역을 선택하는 조건을 간소화합니다. 반복되는 요소와 반복되지 않는 요소를 나눕니다.
  • 우선 select의 내용은 아래와 같이 빼낼 수 있습니다.
const stationItems = [
  "수서",
  "동탄",
  "오송",
  "대전",
  "신경주",
  "울산",
  "부산",
  "공주",
  "익산",
  "목포",
];
  • 그 외에는 label과 v-model 값은 변화하고 나머지는 반복되는 양상을 보입니다. 따라서 아래와 같이 변주되는 부분만을 잡아줄 배열을 만듭니다.
const stationFilteringItems = [
  { label: "출발역", value: "scheduleDepartStation" },
  { label: "도착역", value: "scheduleArriveStation" },
];
  • 그럼 아래와 같이 간결하게 쓸 수 있습니다.
    <v-app id="inspire">
      <v-main class="blue-grey lighten-4">
        <v-container class="mt-5" style="max-width: 700px" fill-height>
          <v-card>
            <div class="pa-15">
              <h1 style="text-align: center" class="mb-10">세부조회 화면</h1>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select v-for="element in stationFilteringItems" :label="element.label" :key="element.label" :items="stationItems" v-model="scheduleInfo[element.value]"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
.
.
.
  • 여기서 한 가지 더 추가할 사항이 있습니다. property의 value를 대괄호로 꺼내서 가져오려면 index signature가 필요합니다. 예를 들어 아래가 기존의 interface라고 해보겠습니다.
export class ScheduleRequest {}
export interface ScheduleRequest {
  scheduleNo?: number;
  scheduleDepartStation: string;
  scheduleArriveStation: string;
  scheduleDate: string;
  scheduleTime: string;
}
  • 하지만 위와 같은 형태로는 대괄호 표기법을 사용할 수 없습니다. 따라서 아래와 같은 내용이 interface에 추가됩니다.
  • 대괄호안에 string type의 key가 들어가면 property의 key에 대응되는 value를 return합니다.
  • index signature는 아무 type의 key나 들어가지 못하게 type을 지정하여 안정성을 더한 type script의 방식입니다.
  • 이 때 return type은 모든 property의 value type을 포괄할 수 있어야 합니다.
export interface ScheduleRequest {
.
.
[key: string]: string | number | undefined;
}
  • 그럼 마저 아래도 짧게 만들어 줍니다.
 <v-row no-gutters>
      <v-col>
        <v-sheet class="pa-2 ma-2">
          <v-select label="성인 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountAdult"></v-select>
        </v-sheet>
      </v-col>
      <v-col>
        <v-sheet class="pa-2 ma-2">
          <v-select label="어린이 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountChild"></v-select>
        </v-sheet>
      </v-col>
      <v-col>
        <v-sheet class="pa-2 ma-2">
          <v-select label="노인 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountElder"></v-select>
        </v-sheet>
      </v-col>
      <v-col>
        <v-sheet class="pa-2 ma-2">
          <v-select label="중증 사람 수" :items="[1, 2, 3]" v-model="headcountInfo.headCountSevere"></v-select>
        </v-sheet>
      </v-col>
  </v-row>
  • 위의 부분에서도 반복되는 부분은 label과 v-model입니다. 따라서 아래와 같이 v-for에서 순회할 배열을 만들어줍니다.
const headcountFilteringItems = [
  { label: "성인 사람 수", value: "headCountAdult" },
  { label: "어린이 사람 수", value: "headCountChild" },
  { label: "노인 사람 수", value: "headCountElder" },
  { label: "중증 사람 수", value: "headCountSevere" },
];
  • 그럼 아래와 같이 짧은 template으로 위의 긴 내용을 대체할 수 있습니다.
<v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select v-for="(element, i) in headcountFilteringItems" :key="i" :label="element.label" :items="[1, 2, 3]" v-model="headcountInfo[element.value]"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
  • 그럼 다시 index signature를 생성해줍시다.
  • 여기서는 모든 property의 value가 number type이기 때문에 union type으로 string type을 포함하지 않아도 됩니다.
export class HeadCount {}
export interface HeadCount {
  headCountAdult: number;
  headCountChild: number;
  headCountSevere?: number;
  headCountElder?: number;
  [key: string]: number | undefined;
}
  • 그럼 최종적으로 아래와 같은 좀 더 간결해진 component가 만들어집니다.
<v-app id="inspire">
      <v-main class="blue-grey lighten-4">
        <v-container class="mt-5" style="max-width: 700px" fill-height>
          <v-card>
            <div class="pa-15">
              <h1 style="text-align: center" class="mb-10">세부조회 화면</h1>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select v-for="element in stationFilteringItems" :label="element.label" :key="element.label" :items="stationItems" v-model="scheduleInfo[element.value]"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-text-field label="날짜" v-model="scheduleInfo.scheduleDate"></v-text-field>
                  </v-sheet>
                </v-col>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select label="시간" :items="timeItems" v-model="scheduleInfo.scheduleTime"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
              <v-row no-gutters>
                <v-col>
                  <v-sheet class="pa-2 ma-2">
                    <v-select v-for="(element, i) in headcountFilteringItems" :key="i" :label="element.label" :items="[1, 2, 3]" v-model="headcountInfo[element.value]"></v-select>
                  </v-sheet>
                </v-col>
              </v-row>
            </div>
            <v-btn type="button" color="blue lighten-1 text-capitalize" depressed large block dark class="mb-3" @click="handleFetchSchedule()"> 조회하기 </v-btn>
          </v-card>
        </v-container>
      </v-main>

      <schedule-result  />
    </v-app>
  • 위에서 table을 따로 분리한 component를 사용하려면 아래와 같이 component를 components scope에 선언하고 import해야 합니다.
  • 그럼 위에서 tag안에 담긴 schedule-result와 같이 kebab case로 사용가능합니다.
import ScheduleResult from '@/views/schedule/ScheduleResult.vue';
export default defineComponent({
  name: 'ScheduleView',
  setup() {
.
.
.
  },
  components: { ScheduleResult },
});

6. props를 활용하기


  • 위에서 분리했던 table component에서 scheduleList라는 것을 활용하고 있습니다.
  • 이를 store에 저장해서 불러올 수도 있지만, 부모-자식 관계를 활용할 수도 있습니다.
  <v-table>
    <tr>
      <td>출발역</td>
      <td>도착역</td>
      <td>출발시간</td>
      <td>도착시간</td>
      <td>기차번호</td>
      <td>특실</td>
      <td>일반실</td>
    </tr>
    <tr v-for="schedule in scheduleList" :key="schedule.scheduleNo">
      <td></td>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </v-table>
  • setup()에서는 아래와 같이 props를 선언하여 활용할 수 있습니다.
  • 사용자가 정의한 custom interface의 경우 아래와 같이 PropType으로 type을 단언(assertion)해줍니다.
  • 이를 통해 해당 변수는 props라서 readonly인 것을 더 명확하게 인지할 수 있습니다.
  • 아래 required 속성을 true로 설정하지 않으면 props가 undefined일 경우를 대비한 처리가 필요합니다.
export default defineComponent({
  name: 'ScheduleResult',
  props: {
    scheduleList: {
      type: Object as PropType<Array<ScheduleResponse>>,
      required: true,
    },
    headcountInfo: {
      type: Object as PropType<HeadCount>,
      required: true,
    },
  },
    setup() {
      .
      .
      .
      },
      components:{
        .
        .
      }
})
  • template에서만 쓸 예정이라면 setup 안으로 가져올 필요가 없습니다.
  • 하지만 script에서 변수로 쓸 생각도 있다면 setup() 안에 매개변수로 props를 적고 꺼내 써야 합니다.
  • props 값을 변경하기 보다는 자식 component에서 props를 활용해 새로운 변수를 만드는 것이 권장됩니다.
    setup(props) {
       let sumHeadCount = props.headcountInfo.headCountAdult + props.headcountInfo.headCountChild + (props.headcountInfo.headCountElder ?? 0);
      .
      .
      .
      onBeforeUpdate(() => {
        sumHeadCount = props.headcountInfo.headCountAdult + props.headcountInfo.headCountChild + (props.headcountInfo.headCountElder ?? 0);
      });
    },
  • child component에서 props를 선언했으므로 이제 parent component에서 child component로 props를 보낼 수 있습니다.
<v-app id="inspire">
      <v-main class="blue-grey lighten-4">
        <v-container class="mt-5" style="max-width: 700px" fill-height>
          <v-card>
            <div class="pa-15">
              <h1 style="text-align: center" class="mb-10">세부조회 화면</h1>
                  .
                  .
                  .
   <schedule-result :scheduleList="scheduleList" :headcountInfo="headcountInfo" />
  • headcountInfo라는 props는 script에서, scheduleList라는 props는 template에서 활용되고 있습니다.
  • template의 경우 props.headcountInfo처럼 property에 접근하지 않아도 바로 활용할 수 있습니다.
  • headCountElder라는 property가 optional이라서 null-coalescing을 활용하였습니다.
  <v-table>
    <tr>
      <td>출발역</td>
      <td>도착역</td>
      <td>출발시간</td>
      <td>도착시간</td>
      <td>기차번호</td>
      <td>특실</td>
      <td>일반실</td>
    </tr>
    <tr v-for="schedule in scheduleList" :key="schedule.scheduleNo">
      <td></td>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </v-table>

//아래는 script
  export default defineComponent({
  name: 'ScheduleResult',
  props: {
    scheduleList: {
      type: Object as PropType<Array<ScheduleResponse>>,
      required: true,
    },
    headcountInfo: {
      type: Object as PropType<HeadCount>,
      required: true,
    },
  },

  setup(props) {
    let sumHeadCount = props.headcountInfo.headCountAdult + props.headcountInfo.headCountChild + (props.headcountInfo.headCountElder ?? 0);
  .
  .

    onBeforeUpdate(() => {
      sumHeadCount = props.headcountInfo.headCountAdult + props.headcountInfo.headCountChild + (props.headcountInfo.headCountElder ?? 0);
    });
    .
    .
  },
});

Tags:

Categories:

Updated:

Leave a comment