refactoring UI issue 해결과정2

뚜비·2023년 10월 7일
0

2023 OSSCA - Argo Workflows

목록 보기
12/13

PR #

서론

지난 시간에 이어 48번 workflows-toolbar를 함수형 컴포넌트로 수정해보겠다.

어떤 UI일까

workflows-toolbar.tsx는 다음과 같다.


Workflow list page에 들어갔을 때

각 workflow의 seleted box를 클릭하면 나오는 toolbar를 의미한다.




cluster, cron, template에 있는 workflows를 모두 실행시키고 전체 체크박스를 눌러서 삭제를 하면

아래 안내판이 표시된다.



본론

코드 분석

interface WorkflowsToolbarProps {
    selectedWorkflows: Map<string, Workflow>; // 선택된 workflows를 전달받음
    loadWorkflows: () => void; // action을 반영한 workflow List를 load함 
    isDisabled: Actions.OperationDisabled; // 사용할 수 있는 버튼인지 
    clearSelection: () => void; // 선택된 workflows를 초기화
}

interface WorkflowGroupAction extends WorkflowOperation { // 해당 Operation에 추가 속성 추가 
    groupIsDisabled: boolean; // 사용할 수 있는 그룹인지 
    className: string; // 이름 
    groupAction: () => Promise<any>;
}

export class WorkflowsToolbar extends React.Component<WorkflowsToolbarProps, {}> {
    constructor(props: WorkflowsToolbarProps) {
        super(props);
    }

    public render() {
        return (
            <Consumer>
                {ctx => (
                    <div className={`workflows-toolbar ${this.getNumberSelected() === 0 ? 'hidden' : ''}`}>
                        <div className='workflows-toolbar__count'>
                            {this.getNumberSelected() === 0 ? 'No' : this.getNumberSelected()}
                            &nbsp;workflow{this.getNumberSelected() === 1 ? '' : 's'} selected
                        </div>
                        <div className='workflows-toolbar__actions'>{this.renderActions(ctx)}</div>
                    </div>
                )}
            </Consumer>
        );
    }

    private getNumberSelected(): number { /* 선택된 workflows 개수를 출력 */ 
        return this.props.selectedWorkflows.size;
    }

    private async performActionOnSelectedWorkflows(ctx: ContextApis, title: string, action: WorkflowOperationAction): Promise<any> {
		/* 선택된 workflow에 대해서 Action을 수행 */ 
		// ctx: ContextApis는 React에서 정의한 Context 

		/* 버튼 선택 후 확인 여부 안내 문구 */
        const confirmed = await ctx.popup.confirm('Confirm', `Are you sure you want to ${title.toLowerCase()} all selected workflows?`); 
        if (!confirmed) { // 안내 문구 취소 선택 시 
            return Promise.resolve(false); // Promise는 성공, 이때 값은 false로 리턴 
        }

		/* 삭제 버튼 시 archived workflows에 대해서 삭제 여부 안내 문구  */
        let deleteArchived = false; 
        if (title === 'DELETE') {
            for (const entry of this.props.selectedWorkflows) { 
                if (isArchivedWorkflow(entry[1])) { // 저장된 workflow인 경우
                    deleteArchived = await ctx.popup.confirm('Confirm', 'Do you also want to delete them from the Archived Workflows database?');
                    break;
                }
            }
        }

		/* 선택한 workflows에 대해서 Action API를 Promise에저장  */
        const promises: Promise<any>[] = []; 
        this.props.selectedWorkflows.forEach((wf: Workflow) => { // 선택한 workflows에 대해서 
            if (title === 'DELETE') {
                // The ones without archivalStatus label or with 'Archived' labels are the live workflows.
                if (isWorkflowInCluster(wf)) { // 클러스터에 존재하는 workflows면 
                    promises.push( 
                        services.workflows.delete(wf.metadata.name, wf.metadata.namespace).catch(reason =>
                            ctx.notifications.show({ // 에러 catch
                                content: `Unable to delete workflow ${wf.metadata.name} in the cluster: ${reason.toString()}`,
                                type: NotificationType.Error
                            })
                        )
                    );
                }
                if (deleteArchived && isArchivedWorkflow(wf)) {  // deleteArchive에 존재하고 archive된 workflow라면?
                    promises.push(
                        services.workflows.deleteArchived(wf.metadata.uid, wf.metadata.namespace).catch(reason =>
                            ctx.notifications.show({
                                content: `Unable to delete workflow ${wf.metadata.name} in database: ${reason.toString()}`,
                                type: NotificationType.Error
                            })
                        )
                    );
                }
            } else {
                promises.push( // 나머지 경우에 대해서 
                    action(wf).catch(reason => {
                        this.props.loadWorkflows();
                        ctx.notifications.show({
                            content: `Unable to ${title} workflow: ${reason.content.toString()}`,
                            type: NotificationType.Error
                        });
                    })
                );
            }
        });
        return Promise.all(promises); // promise 병렬 수행, 각 action에 대한 각 workflow의 action들이 담기고 이를 비동기로 처리하여 수행한 결과를 리턴하게 된다. 
    }

    private renderActions(ctx: ContextApis): JSX.Element[] { // 수행할 수 있는 버튼에 대해 조사 후 render 
		/* --- renderAction 처리를 위한 로컬 변수 선언 --- */
        const actionButtons = []; // 빈 배열 선언 
        const actions: any = Actions.WorkflowOperationsMap; // 여러 연산들(RETRY, RESUBMIT, SUBMIT 등)의 Map
        const disabled = this.props.isDisabled; // 사용여부 변수 배열, 이름마다 담겨있음
        const groupActions: WorkflowGroupAction[] = Object.keys(actions).map((actionName: WorkflowOperationName) => { // actions의 keys만 가져온 후 map으로 묶음
            const action = actions[actionName]; // action 하나에 대해서
            return {
                title: action.title, // WorkflowOperation에 정의된 것
                iconClassName: action.iconClassName, // WorkflowOperation에 정의된것
                groupIsDisabled: disabled[actionName], // 선택된 workflow(=group)에 대한 사용여부, 우리가 굳이 사용 여부 판단할 필요는 없음(workflows-list에서 처리)
                action, // WorkflowOperaion에 정의된 것, 딱히 필요없어보임
                groupAction: async () => { // 선택된 workflow(=group)에 대해 처리할 액션 담기  
                    const confirmed = await this.performActionOnSelectedWorkflows(ctx, action.title, action.action); // performActionOnSelectedWorkflows 수행 후 값을 리턴 받음
                    if (!confirmed) { // false를 받았을 때 아무처리 안 함 
                        return; 
                    }

                    this.props.clearSelection(); // 선택된 workflow 해제, workflow-list에서 selectedWorkflows를 비우는 함수를 진행
                    ctx.notifications.show({
                        content: `Performed '${action.title}' on selected workflows.`,
                        type: NotificationType.Success
                    });
                    this.props.loadWorkflows(); // 다시 workflows를 load함
                },
                className: action.title, // 위와 동일
                disabled: () => false 
            } as WorkflowGroupAction;
        });

		/*--- 각 action에 대해서 각종 설정(사용가능여부, 클릭이벤트 처리) 해놓고 추가 ---*/
        for (const groupAction of groupActions) { // 즉 처리될 버튼에 따라 action이 다르므로 
            actionButtons.push(
                <button
                    key={groupAction.title}
                    onClick={() => {
                        groupAction.groupAction().catch(); // 성공시 groupAction()만 실패시 catch()까지
                    }}
                    className={`workflows-toolbar__actions--${groupAction.className} workflows-toolbar__actions--action`}
                    disabled={this.getNumberSelected() === 0 || groupAction.groupIsDisabled}>
                    <i className={groupAction.iconClassName} />
                    &nbsp;{groupAction.title}
                </button>
            );
        }
        return actionButtons;
    }
}
  • workflowOperation
export interface WorkflowOperation {
    title: WorkflowOperationName; // type으로 정의 'RETRY','RESUBMIT'.'SUSPEND'.'RESUME','STOP','TERMINATE','DELETE' 가 있음 
    action: WorkflowOperationAction; // 버튼 클릭 시 발생하는 action 
    iconClassName: string; // 버튼안에 같이 쓰일 icon 이름 
    disabled: (wf: Workflow) => boolean; // 사용가능 여부 
}
  • workflowGroupAction
    workflowOperation을 상속받음
interface WorkflowGroupAction extends WorkflowOperation {
    groupIsDisabled: boolean; // group(선택된 workflow)의 사용 가능 여부
    className: string; 
    groupAction: () => Promise<any>; // 모든 버튼의 Promise 객체 
}
  • workflowOperationAction
    workflowOperation의 action 변수의 타입
export type WorkflowOperationAction = (wf: Workflow) => Promise<Workflow | WorkflowDeleteResponse>;
// eg. (wf: Workflow) => services.workflows.retry(wf.metadata.name, wf.metadata.namespace, null)
  • renderAction의 로컬 변수들
  1. actionButtons : 버튼의 action, disable에 바탕으로 <button> 객체를 모으는 배열
  2. actions : WorkflowOperation들을 정의한 곳 / RETRY, RESUBMIT 등의 Operion들이 workflowOperion 인터페이스를 상속받아 정의됨
export const WorkflowOperationsMap: WorkflowOperations = {
    RETRY: {
        title: 'RETRY',
        iconClassName: 'fa fa-undo',
        disabled: (wf: Workflow) => {
            const workflowPhase: NodePhase = wf && wf.status ? wf.status.phase : undefined;
            return workflowPhase === undefined || !(workflowPhase === 'Failed' || workflowPhase === 'Error');
        },
        action: (wf: Workflow) => services.workflows.retry(wf.metadata.name, wf.metadata.namespace, null)
    },
    RESUBMIT: {
        title: 'RESUBMIT',
        iconClassName: 'fa fa-plus-circle',
        disabled: () => false,
        action: (wf: Workflow) => services.workflows.resubmit(wf.metadata.name, wf.metadata.namespace, null)
    },
  1. disabled : 각 버튼이 사용 가능한지 여부(true or false)를 담은 배열로 props의 isDisabled를 받아옴, 타입은 OperationDisabled에 정의됨
export type OperationDisabled = {
    [action in WorkflowOperationName]: boolean;
};
  1. groupActions : 각 Operation에 대해 선택된 workflow(selected Worfklows)에 대해 다시 정의해서 return 되는 WorkflowGroupOperation 배열임


해결해야 할 과제

중간에 Promise 객체는 무엇이며 어떻게 처리할까?

  • 자바스크립트는 싱글 스레드 언어
    자바스크립트 작동 원리
    자바스크립트 비동기 처리 - 그림
    자바스크립트 비동기 처리
  • Promise 객체
    호다닥 톺아보는 Promise이기 때문에 함수를 실행할 수 있는 창구가 단 하나이고, 동시에 2개 이상의 함수를 동시에 실행시킬 수 없다. Call Stack을 통해서 함수를 처리하는데, Call Stack에서 제거되기 전까지는 어떤 태스크도 실행되지 않는다.
    즉 자바스크립트에서 비동기로 처리해야 한다면 자신이 처리하지 않고 Web API에게 위임하는데 이때 처리된 내용은 Callback Queue(Task Queue)에 들어감!!!!!!!
    이때 큐에서 요소들이 빠져나가려면 이벤트 루프가 등장하는데 Call Stack을 지속적으로 보다가 Call Stack이 비면 우선순위에 따라 Queue들에서 대기 중인 요소들을 call Stack에 넣어줌

    자바스크립트에서 비동기처리가 필요한 이유
    화면에서 서버로 데이터를 요청했을 때 서버가 언제 그 요청에 대한 응답을 할지도 모르는 상태에서 다른 코드를 실행 안하고 기다릴 수는 없기 때문이다.


(내가 생각한 비동기 그냥 동시에 실행 가능 한 것? 동기는 순차적으로 실행 가는한 것 -> 간단하게 함수 실행 시 응답(ex.값)이 보장되면 동기 보장이 안 되면 비동기, 즉 비동기 함수 코드가 종료되지 않았어도 대기하지 않고 다음 코드를 실행하는게 비동기)
: 비동기 처리를 위한 javascript 객체로 비동기로 실행된 작업의 결과를 나타내는 객체의 이름
: Promise가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자, 비동기 연산이 종료된 이후에 결과값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있음
: executor라는 함수를 만들어서 비동기(시작 시간이 상관없음)로 실행, 성공이든 실패든 값을 반환 / 이때, 두 개의 callback 파라미터를 갖고 있는데 resolve(value)는 함수 안의 처리가 끝났을 때 성공시 value를 반환하는 함수를 호출, reject(error)는 실패시 error를 반환하는 함수
: then(메서드) - callback 함수를 인자로 받는 애 (성공과 실패에 대한 처리 모두 가능) 그러나 보통 resolve가 호출되면 실행
: catch - error 처리 부분 reject가 호출되면 실행

  • Promise도 결국 then을 이용하는데 좀 더 쉽게 처리하기 위해서는 async와 await을 사용
    async : 암시적으로 Promise 객체를 반환
    awiat : 기다려주는 함수
    try / catch : reject 잡는 부분

  • Promise 객체가 사용되는 부분은 perfomAction 부분 박에 없는데.. 즉 Promise.all 메소드를 통해 해당 action에 대한 각 workflows 처리를 동시해 실행시킨다.
    이 부분을 어떻게 처리하느냐가 관건인듯

  • 윤우님께 여쭤보다




즉 Prmoise.all은 그대로 두되 다른 애들을 처리한다.



해결과정

  1. 함수형으로 다 바꾸기
  • props는 매개변수로 받기
  • 메소드들은 inner function으로 바꾸기
  1. WorkflowsToolbarProps 수정
    loadWorkflows()는 변경된 workflow list를 불러오는 함수를 전달받는 것인데 결국 ListWatch 객체에서 업데이트하기 때문에 삭제한다. loadWorkflows()의 역할은 select를 다 해제하고 workflows를 load하는 역할임 그리고 props.loadWorkflows()를 호출하는 부분도 다 삭제!

따라서 최종적으로 다음과 같은 props가 존재한다.

interface WorkflowsToolbarProps {
    selectedWorkflows: Map<string, Workflow>;
    isDisabled: Actions.OperationDisabled;
    clearSelection: () => void;
}

  1. getNumberSelected() 수정하기
    WorkflowsToolBar가 계속 rendering 되면 이 getNumberSelected도 계속 호출하게 된다...
function getNumberSelected(): number {
        console.log('나 호출됨');
        return props.selectedWorkflows.size;
    }


콘솔창에 로그가 찍히도록 두니까 역시나 체크박스를 다르게 클릭할 때마다 계속 로그가 찍힌다. 즉 코드에 보면 rendering할 때 계속 이 getNuberSelected()함수가 계속 호출된다..


  • useEffect를 추가해볼까? NoNo 그럴필요 없다.
    공식문서를 그대로 번역하심
    useEffect는 해당 dependency(여기서는 selectedWorkflows)가 변경될 때만 useEffect의 callback 함수를 실행시켜준다. 이때(dependency의 주소의 변경을 check한다)

근데.. 내가 얻고 싶은 값은 부모 컴포넌트에서 변화가 일어나고 그 값을 받는다.
즉 부모에서 seletedWorkflows에 변화가 일어나면 이를 ToolBar 컴포넌트에 props를 전달하기 때문에 렌더링이 되어야 한다.
React는 정확히 언제 렌더링? 글을 참고!

따라서 함수호출을 할 필요 없이 그냥 Toolbar 컴포넌트에 변수를 할당한 후 해당 변수를 전달 받는 것이 효율적이다. (어차피 리렌더링 되면 props를 다시 받아와야 하니까!

const numberSelected: number = props.selectedWorkflows.size;

요렇게 정의했다.


  1. Context를 제거
    지금 Consumer 라는 객체로 감싸져있는 것을 확인할 수 있는데
    간단히 이야기하면 Context 객체는 부모가 자식에게 props를 통해 데이터를 전달했는데 너무 복잡한 나머지.. 객체로 정의한 것이다. 상위 컴포넌트에서 Provider로 데이터를 전달하고 하위 컴포넌트에서는 Consumer로 데이터를 사용한다.
    Context API

return (
        <Consumer>
            {ctx => (
                <div className={`workflows-toolbar ${numberSelected === 0 ? 'hidden' : ''}`}>
                    <div className='workflows-toolbar__count'>
                        {numberSelected === 0 ? 'No' : numberSelected}
                        &nbsp;workflow{numberSelected === 1 ? '' : 's'} selected
                    </div>
                    <div className='workflows-toolbar__actions'>{renderActions(ctx)}</div>
                </div>
            )}
        </Consumer>
    );

지금 렌더링 파트를 보면 < Consumer > 객체를 확인할 수 있는데 Consumer 객체는 ctx라는 일종의 props 값을 전달하고 이 ctx에서 popup과 notification을 사용한다.

// 이것이 context.tx
import {AppContext as ArgoAppContext, NavigationApi, NotificationsApi, PopupApi} from 'argo-ui';
import {History} from 'history';
import * as React from 'react';

export type AppContext = ArgoAppContext & {apis: {popup: PopupApi; notifications: NotificationsApi; navigation: NavigationApi; baseHref: string}};

export interface ContextApis {
    popup: PopupApi;
    notifications: NotificationsApi;
    navigation: NavigationApi;
    history: History;
}

export const Context = React.createContext<ContextApis>(null);
export const {Provider, Consumer} = Context;

우리에겐.. useContext가 있지요 후후
먼저 여기서 사용하려는 context가 다 ContextApi이고 이는 context.ts에서 React.createContext<ContextApis>(null)로 컨텍스트가 생성되었기 때문에 생성된 Context를 import를 통해 부르고 useContext를 통해서 popup과 notification 값을 가져온다.

import {Context} from '../../../shared/context';

export function WorkflowsToolbar(props: WorkflowsToolbarProps) {
    const {popup, notifications} = useContext(Context);

이렇게 사용해서 파라미터에 ctx가 있는 부분을 다 제거해주고 popup과 notification만 사용해도 가능가능

그리고

return (
        <div className={`workflows-toolbar ${numberSelected === 0 ? 'hidden' : ''}`}>
            <div className='workflows-toolbar__count'>
                {numberSelected === 0 ? 'No' : numberSelected}
                &nbsp;workflow{numberSelected === 1 ? '' : 's'} selected
            </div>
            <div className='workflows-toolbar__actions'>{renderActions()}</div>
        </div>
    );

위와 같이 return을 수정해줘도 된다.


  1. renderActions를 분리하자
function renderActions(): JSX.Element[] {
        const actionButtons = [];
        const actions: any = Actions.WorkflowOperationsMap;
        const disabled = props.isDisabled;
        const groupActions: WorkflowGroupAction[] = Object.keys(actions).map((actionName: WorkflowOperationName) => {
            const action = actions[actionName];
            return {
                title: action.title,
                iconClassName: action.iconClassName,
                groupIsDisabled: disabled[actionName],
                action,
                groupAction: async () => {
                    const confirmed = await performActionOnSelectedWorkflows(action.title, action.action);
                    if (!confirmed) {
                        return;
                    }

                    props.clearSelection();
                    notifications.show({
                        content: `Performed '${action.title}' on selected workflows.`,
                        type: NotificationType.Success
                    });
                },
                className: action.title,
                disabled: () => false
            } as WorkflowGroupAction;
        });
        for (const groupAction of groupActions) {
            actionButtons.push(
                <button
                    key={groupAction.title}
                    onClick={() => {
                        groupAction.groupAction().catch();
                    }}
                    className={`workflows-toolbar__actions--${groupAction.className} workflows-toolbar__actions--action`}
                    disabled={numberSelected === 0 || groupAction.groupIsDisabled}>
                    <i className={groupAction.iconClassName} />
                    &nbsp;{groupAction.title}
                </button>
            );
        }
        return actionButtons;
    }

지금 renderActions 함수는 groupAction 각각에 대해서 정의한 변수들을 담은 groupActions 변수를 이용해서 각 action에 대한 버튼을 부여한 button 태그를 반환하고 있다. render하는 부분과 변수들을 정의하는 부분을 분리하는게 필요하다!!


우선 button 객체 생성하는 부분은 renderActions() 대신 다음이 들어간다.

{groupActions.map(action => {
                    return (
                        <button
                            key={action.title}
                            onClick={() => {
                                action.groupAction().catch();
                            }}
                            className={`workflows-toolbar__actions--${action.className} workflows-toolbar__actions--action`}
                            disabled={numberSelected === 0 || action.groupIsDisabled}>
                            <i className={action.iconClassName} />
                            &nbsp;{action.title}
                        </button>
                    );
                })}

그리고 renderAcitons()에 들어가는 지역변수들은 WorkflowsToolbar 컴포넌트 변수로 넘겼다.

  • actionButtons : 삭제
  • actions : 전역변수로 넘김
  • disabled : groupactions의 지역변수로 넘김 (Why selectWorkflows의 영향을 받기 때문에 )
  • groupActions : props.selectWorfklows의 영향을 받기 때문에 useMemo를 사용해서 값을 정의했다.
const groupActions = useMemo<WorkflowGroupAction[]>(() => {
        const disabled = props.isDisabled;
        return Object.keys(actions).map((actionName: WorkflowOperationName) => {
            const action = actions[actionName];
            return {
                title: action.title,
                iconClassName: action.iconClassName,
                groupIsDisabled: disabled[actionName],
                action,
                groupAction: async () => {
                    const confirmed = await performActionOnSelectedWorkflows(action.title, action.action);
                    if (!confirmed) {
                        return;
                    }

                    props.clearSelection();
                    notifications.show({
                        content: `Performed '${action.title}' on selected workflows.`,
                        type: NotificationType.Success
                    });
                },
                className: action.title,
                disabled: () => false
            } as WorkflowGroupAction;
        });
    }, [props.selectedWorkflows]);

이때 renderAcitons 자체가 이 ToolBar가 렌더링되면 호출되는데, seletedWorkflows이 바뀜에 따라 렌더링되면 호출된다..
useEffect 함수 안에 groupAction을 지정하도록 할까 고민이었다.
그러나 useEffect의 경우 dependency 뿐만 아니라 생성될 때 언마운트 될 때 모두 호출하기 때문에
useMemo를 설정해두었다. 그런데 useMemo도 과연 활용하는게 맞나 싶은게 renderAction 자체가 selectedWorkflows가 바뀜에 따라 호출되는 아이인디 그냥 원래 바뀌잖슴??? useEffect, useMemo

  • TEST 해보자
    useMemo를 사용하지 않았을 때


mount되었을 때 7호출됨이 8개가 호출되었고 그 이후 selectedWorkflows에 변화가 있을 때 7호출이 쌓였다.

참고로 delete 이후 UI 업데이트가 안 되는 이슈에서 select 버튼을 누르면 workflow가 사라지는데 이 떄 저 toolbar는 select를 누른 상태를 유지하고 있어서 계속 저런 화면이 뜬다.( 정확히 말하면 selected Workflows에서 변화가 없기 때문에 )

또한 참고로 호출됨이 왜 하필 7번일까를 생각해보면 지금 toolbar 버튼이 7개이기 때문에 7개에 대해서 selected를 확인해야 하기 때문이다!!


  • useMemo 사용 시
const groupAction = useMemo<WorkflowsOperation[]>(() => {
        console.log('호출됨');
        const disabled = props.actionsIsDisabled;
        return Object.keys(actions).map((actionName: WorkflowOperationName) => {
            const action = actions[actionName];
            console.log('호출됨22');
            return {
                title: action.title,
                iconClassName: action.iconClassName,
                isDisabled: disabled[actionName],
                action,
                workflowsAction: async () => {
                    const confirmed = await performActionOnSelectedWorkflows(action.title, action.action);
                    if (!confirmed) {
                        return;
                    }

                    props.clearSelection();
                    notifications.show({
                        content: `Performed '${action.title}' on selected workflows.`,
                        type: NotificationType.Success
                    });
                },
                disabled: () => false
            } as WorkflowsOperation;
        });
    }, [props.selectedWorkflows]);


한 번 Mount 될 때 selectWorkflows에 변화가 있을 때 호출됨과 호출됨22가 각각 1번, 7번씩 호출된다.


위와 다른 점은 useMemo 때는 Mount 시 7호출됨이 한 번만 발생한다는 점이다.


  • useEffect 사용 시
const disabled = props.actionsIsDisabled;
    const groupAction = Object.keys(actions).map((actionName: WorkflowOperationName) => {
        const action = actions[actionName];
        console.log('뭥미');
        return {
            title: action.title,
            iconClassName: action.iconClassName,
            isDisabled: disabled[actionName],
            action,
            workflowsAction: async () => {
                const confirmed = await performActionOnSelectedWorkflows(action.title, action.action);
                if (!confirmed) {
                    return;
                }

                props.clearSelection();
                notifications.show({
                    content: `Performed '${action.title}' on selected workflows.`,
                    type: NotificationType.Success
                });
            },
            disabled: () => false
        } as WorkflowsOperation;
    });
    useEffect(() => {
        console.log('여기서도 호출되구');
        groupAction;
    }, []);


뭐야 useMemo랑 아예 없는거랑 합친거나 다름 없...


useMemo의 경우 selectedWorkflows가 자주 바뀌면 의미가 없다고 한다. 그래서 이걸 사용하는게 의미가 있는지는 모르겠지만 우선 useMemo react Hook을 사용한 후 사용할 필요가 있을지 물어보도록 한다.


  1. 변수들의 이름에 대해서 다시 생각해보자.
    interface workflowGroupAction 은 음.. 여기서 group이 workflow를 의미하는지, Action을 의미하는지 모호하다...
    내 생각에는 workflowOperation을 상속받고 있기 때문에 workflowOperation은 하나의 workflow에 대한 것이기에 이는 WorkflowGroup을 의미하는 것 같다.
  • WorkflowGroupAction -> WorkflowsOperation이 더 안 헷갈리고 상속받는 것과 일맥상통할 듯
  • groupIsDisabled는 선택된 workflows에 대해서 해당 버튼(operation or action)이 사용가능한지 여부를 알 수 있는 변수임 관련있기 때문에 groupIsDisabled -> IsDisabled가 더 정확할 듯
    (기존의 workflowsToolbarProps의 isDisabled-> actionsIsDisabled로 바꿈)
  • groupAction -> workflowsAction으로
  • className -> 걍 제거 action.title가 동일
interface WorkflowsAction extends WorkflowOperation {
    actionIsDisabled: boolean;
    workflowsAction: () => Promise<any>;
}
  1. performActionOnSelectedWorkflows

    [https://han41858.tistory.com/11](Promise 생성하는 방법)
    해당 메소드는 액션을 수행하는 메소드인데 해당 함수는 Prmoise를 리턴한다.
  • 선택된 workflows를 action처리할거니? -> 취소를 누르면 Promise.resolve(false)가 나오는데 false라는 값을 가진 Promise 객체가 만들어진다.

  • workflow 중에 아카이브에 저장된 workflow가 하나라도 있다면 -> 삭제 할거니 물어보고 -> 데이터 받아온다.

  • Promise 객체에 담고 각각 선택된 workflows에 대해서 action을 수행하면 된다.

  • async/await 함수 분석
    async/await 분석2

async function asyncCall() {
  return "Hi";
}

function asyncCallTwo() {
  return Promise.resolve("Hi");
}

function main(){
  const t1 = asyncCall();
  const t2 = asyncCallTwo();
  if(t1){
    console.log("t1 호출");
    console.log(t1);
  }

  if(t2){
    console.log("t2 호출");
    console.log(t2);
  }
}

main();
> "t1 호출"
> [object Promise]
> "t2 호출"
> [object Promise]

위의 결과가 한 번에 출력된다.

  • async function은 항상 Promise 객체를 리턴한다.
    따라서 asyncCall()의 리턴값이 "Hi"임에도 Promise 객체가 출력된다.
    asyncCall()은 asyncCallTwo()와 같다는 것을 알 수 있다.

우리가 원하는 리턴값을 얻고 싶으면 어떻게 해야할까?

async function asyncCall() {
  return await "Hi";
}

function asyncCallTwo() {
  return Promise.resolve("Hello");
}

async function main(){ // async 키워드 추가 
  const t1 = await asyncCall(); // await 추가
  const t2 = await asyncCallTwo(); // await 추가
  
  if(t1){
    console.log("t1 호출");
    console.log(t1);
  }

  if(t2){
    console.log("t2 호출");
    console.log(t2);
  }
}

main();
> "t1 호출"
> "Hi"
> "t2 호출"
> "Hello"
  • 즉 promise 객체를 반환하는 함수(async 함수) 앞에 await 키워드가 있다면 resolve안의 값을 리턴한다.

  • 잠깐!! async가 붙었다고 다 비동기 함수는 아니라고?!
    공식문서를 보면 다음과 같이 적혀있다.

    async 함수의 본문은 0개 이상의 await 문으로 분할된 것으로 생각할 수 있습니다. 첫번째 await 문을 포함하는 최상위 코드는 동기적으로 실행됩니다. 따라서 await 문이 없는 async 함수는 동기적으로 실행됩니다. 하지만 await 문이 있다면 async 함수는 항상 비동기적으로 완료됩니다.

즉 async 함수라도 동기적으로 실행되었다가 await 키워드가 존재하는 순간 부터 비동기적으로(순서없이 실행!!)된다는 의미이다.


그냥 performed acction에는 삭제할 때 필요한 명령어들만 두고 알림판은 위로 따로 구분지었다.

workflowsAction: async () => {
                    //check for action
                    const confirmed = await popup.confirm('Confirm', `Are you sure you want to ${action.title.toLowerCase()} all selected workflows?`);
                    if (!confirmed) {
                        return;
                    }

                    // check for delete from archived workflows
                    let deleteArchived = false;
                    if (action.title === 'DELETE') {
                        // check for delete workflows from archived workflows
                        for (const entry of props.selectedWorkflows) {
                            if (isArchivedWorkflow(entry[1])) {
                                deleteArchived = await popup.confirm('Confirm', 'Do you also want to delete them from the Archived Workflows database?');
                                break;
                            }
                        }
                    }

                    performActionOnSelectedWorkflows(action.title, action.action, deleteArchived);

                    props.clearSelection();
                    notifications.show({
                        content: `Performed '${action.title}' on selected workflows.`,
                        type: NotificationType.Success
                    });
                },
                disabled: () => false
            } as WorkflowsOperation;
        });
    }, [props.selectedWorkflows]);
function performActionOnSelectedWorkflows(title: string, action: WorkflowOperationAction, deleteArchived: boolean): Promise<any> {
        const promises: Promise<any>[] = [];
        props.selectedWorkflows.forEach((wf: Workflow) => {
            if (title === 'DELETE') {
                // The ones without archivalStatus label or with 'Archived' labels are the live workflows.
                if (isWorkflowInCluster(wf)) {
                    promises.push(
                        services.workflows.delete(wf.metadata.name, wf.metadata.namespace).catch(reason =>
                            notifications.show({
                                content: `Unable to delete workflow ${wf.metadata.name} in the cluster: ${reason.toString()}`,
                                type: NotificationType.Error
                            })
                        )
                    );
                }
                if (deleteArchived && isArchivedWorkflow(wf)) {
                    promises.push(
                        services.workflows.deleteArchived(wf.metadata.uid, wf.metadata.namespace).catch(reason =>
                            notifications.show({
                                content: `Unable to delete workflow ${wf.metadata.name} in database: ${reason.toString()}`,
                                type: NotificationType.Error
                            })
                        )
                    );
                }
            } else {
                promises.push(
                    action(wf).catch(reason => {
                        notifications.show({
                            content: `Unable to ${title} workflow: ${reason.content.toString()}`,
                            type: NotificationType.Error
                        });
                    })
                );
            }
        });
        return Promise.all(promises);
    }

profile
SW Engineer 꿈나무 / 자의식이 있는 컴퓨터

0개의 댓글