왜 class 메서드의 this 값이 undefined로 나올까요?

김철준·2024년 11월 25일
0

Trouble Shooting

목록 보기
2/3
post-thumbnail

자바스크립트에서의 this는 함수 호출방식에 따라 가르키는 값이 달라집니다.

로그인 API 함수와 관련된 함수를 응집도있게 사용하게 위해 class를 통해 API 관련 함수들을 관리하고 있습니다.

아래와 같이 로그인 API 함수와 API 함수 응답값에 따른 처리에 대한 함수를 class 메서드로 사용하고 있는데요. 코드는 다음과 같아요.

import {authStorage} from "../../helper/auth/authStorage.ts";
import {AUTH} from "../domainPath.ts";
import networkInstance from "../network.instance.ts";
import {AbstractAuthService} from "./interface.auth.ts";
import {LoginRequest, SignupRequest} from "./types.ts";

// 로그인,회원가입 추상화 인터페이스 구현한 클래스
class AuthService implements AbstractAuthService{



    async login(request: LoginRequest): Promise<boolean> {
        const response = await networkInstance(`${AUTH}/login`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
        })

       return  await this.afterAuthAction(response)
    }



    async signup(request: SignupRequest): Promise<boolean> {
        const response = await  networkInstance(`${AUTH}/create`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
        })
       return  await this.afterAuthAction(response)

    }

    private async afterAuthAction(response:Response): Promise<boolean> {
        const result = await response.json();
        if (!response.ok) {
            console.error(result.details);
            throw new Error(result.details);
        }
        authStorage.setToken(result.token);
        return true;
    }
}

export const authService = new AuthService();


컴포넌트에서는 인스턴스 메서드로 관련 API 함수를 사용하고 있어요. 컴포넌트는 다음과 같습니다.

import AuthForm, {AuthFormType} from "../../components/feature/auth/authForm.tsx";
import {AUTH_PAGE_ENUM} from "../../constant/feature/auth/constant.ts";
import {executeNetworkRequest, handleNetworkError, handleNetworkSuccess} from "../../helper/networkUtils.ts";
import {authService} from "../../service/auth/api.auth.ts";

/**
 * 로그인 페이지
         */
function Login() {
    async function networkLogin(form:AuthFormType) {
    return executeNetworkRequest<AuthFormType,boolean>({
        requestFunction: authService.login,
        requestParams: form,
        onSuccess:() => handleNetworkSuccess({alertMessage:"로그인 성공",redirectUrl:"/todo"}),
        onError: handleNetworkError,
    })


    }
    return (
        <AuthForm
            pageType={AUTH_PAGE_ENUM.LOGIN}
            networkRequest={networkLogin}/>
    );
}

export default Login;


import {FLEX_COLUMN_CONTAINER_CLASSNAME} from "../../../constant/css/constant.ts";
import {AUTH_PAGE_ENUM, AUTH_PAGE_TYPE} from "../../../constant/feature/auth/constant.ts";
import "../../../css/auth/authForm.css"
import "../../../css/index.css"
import useAuthForm from "../../../helper/auth/useAuthForm.ts";
import AbstractForm, {AbstractButtonType} from "../../form/abstractForm.tsx";

export interface AuthFormType {
    email: string;
    password: string;
}
function AuthForm({networkRequest,pageType}:{
    networkRequest:(form:AuthFormType)=>Promise<void> // API 요청 함수
    pageType: AUTH_PAGE_TYPE // 페이지 타입
}) {

    const {form,fields}=useAuthForm()

    async function handleSubmit() {
      await networkRequest(form)
    }

    const button:AbstractButtonType[]=[{
        label: AUTH_PAGE_ENUM.SIGNUP === pageType ? "회원가입" : "로그인",
        type: "submit",
        disabled: false,
    }]


    return (
        <div className={FLEX_COLUMN_CONTAINER_CLASSNAME}>
            <h1>{pageType===AUTH_PAGE_ENUM.SIGNUP?"회원가입":"로그인"}</h1>
            <AbstractForm
                onSubmit={handleSubmit}
                className={`${FLEX_COLUMN_CONTAINER_CLASSNAME} auth-form-inner-container`}
            >
               <AbstractForm.Fields fields={fields}></AbstractForm.Fields>
                <AbstractForm.Buttons buttons={button}/>
            </AbstractForm>

        </div>
    );
}

export default AuthForm;

API 함수들의 성공,실패 처리에 대한 추상화 함수를 통해 API 함수를 호출하고 있어요. executeNetworkRequest 함수의 requestFunction이라는 인자에 콜백함수를 할당함으로써 API 함수를 호출하죠.

// network 요청 실행
export async function executeNetworkRequest<P, T>({
                                                      requestFunction,
                                                      requestParams,
                                                      onSuccess,
                                                      onError,
                                                  }: {
    requestFunction: (params: P) => Promise<T> | (() => Promise<T>); // 인자가 없을 수도 있음
    requestParams?: P; // 선택적 인자
    onSuccess: () => void;
    onError: (error: unknown) => void;
}) {
    try {
        const success =
            requestParams !== undefined
                ? await (requestFunction as (params: P) => Promise<T>)(requestParams) // 인자 있는 경우
                : await (requestFunction as () => Promise<T>)(); // 인자 없는 경우
        if (success) {
            onSuccess();
        }
    } catch (error) {
        onError(error);
    }

로그인 버튼을 눌러 로그인을 시도해볼까요?

하지만 위와 같이 코드를 구성하고 로그인을 하게 되면 다음과 같은 에러가 발생해요.

무엇이 문제일까?

afterAuthAction함수는 위에서 로그인 API 응답값에 대해 처리하는 class 메서드였죠? 하지만 이를 읽을 수 없다고 합니다.

import {authStorage} from "../../helper/auth/authStorage.ts";
import {AUTH} from "../domainPath.ts";
import networkInstance from "../network.instance.ts";
import {AbstractAuthService} from "./interface.auth.ts";
import {LoginRequest, SignupRequest} from "./types.ts";

// 로그인,회원가입 추상화 인터페이스 구현한 클래스
class AuthService implements AbstractAuthService{

    async login(request: LoginRequest): Promise<boolean> {
        const response = await networkInstance(`${AUTH}/login`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
        })

       return  await this.afterAuthAction(response)
    }



...

    private async afterAuthAction(response:Response): Promise<boolean> {
        const result = await response.json();
        if (!response.ok) {
            console.error(result.details);
            throw new Error(result.details);
        }
        authStorage.setToken(result.token);
        return true;
    }
}

export const authService = new AuthService();


디버깅을 해보면 아래 부분에서 문제가 발생하고 있어요.

    async login(request: LoginRequest): Promise<boolean> {
        const response = await networkInstance(`${AUTH}/login`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
        })

        console.log("this : ", this) // undefined


       return  await this.afterAuthAction(response)
    }

this.afterAuthAction(response) 이 부분이죠.

console로 this를 찍어볼까요?

undefined입니다. undefined의 afterAuthAction을 읽으려니 에러가 발생한거죠.
하지만 왜 그럴까요? 제가 알기론 class 내부에서 사용하는 this는 인스턴스를 가르키는 것으로 알고 있는데 말이죠.

제가 this에 대해서 공부를 덜 했기 때문에 이러한 실수를 한거였어요.
이유는 다음과 같아요.

콜백함수로 함수가 호출되면 this는 undefined로 인식된다.

그전에 알아둘것은 함수 내부에서 this는 일반적으로 전역객체 window를 가르킵니다. 하지만 strict mode에서는 undefined로 값을 나타내죠. 클래스 내부의 모든 코드에는 strict mode가 암묵적으로 적용됩니다.

이유는 콜백함수로 함수가 호출되면 this는 undefined로 인식되기 때문이에요. 위에서 말한것처럼 API 함수를 호출할 때,executeNetworkRequest라는 함수의 콜백함수로 한번 걸쳐서 사용한다고 했죠?

// network 요청 실행
export async function executeNetworkRequest<P, T>({
                                                      requestFunction,
                                                      requestParams,
                                                      onSuccess,
                                                      onError,
                                                  }: {
    requestFunction: (params: P) => Promise<T> | (() => Promise<T>); // 인자가 없을 수도 있음
    requestParams?: P; // 선택적 인자
    onSuccess: () => void;
    onError: (error: unknown) => void;
}) {
    try {
        const success =
            requestParams !== undefined
                ? await (requestFunction as (params: P) => Promise<T>)(requestParams) // 인자 있는 경우
                : await (requestFunction as () => Promise<T>)(); // 인자 없는 경우
        if (success) {
            onSuccess();
        }
    } catch (error) {
        onError(error);
    }
}


async function networkLogin(form:AuthFormType) {
    return executeNetworkRequest<AuthFormType,boolean>({
        requestFunction: authService.login,
        requestParams: form,
        onSuccess:() => handleNetworkSuccess({alertMessage:"로그인 성공",redirectUrl:"/todo"}),
        onError: handleNetworkError,
    })


    }

그래서 executeNetworkRequest를 호출할 때,콜백함수로 authService라는 class 인스턴스의 login 메서드를 넣어주고 있어요.

하지만 인스턴스의 메서드에서 this를 사용할지언정, 콜백함수의 자리에서 해당 메서드가 호출이 된다해도 this는 class를 기억하지않고 연결이 끊겨버립니다.

왜냐하면 this는 함수가 호출되는 방식에 따라 this가 가르키는 값이 동적으로 결정되니까요.

콜백함수 내부에서의 this가 가르키는 값은 undefined입니다. 따라서 authService라는 class 인스턴스의 loign 메서드 함수를 할당해줘도 이제 this가 undefined일 뿐인거죠.

그러면 어떻게 해결할까요?

저는 2가지 방법 정도를 찾았어요.
1. Function.prototype.bind
2. 화살표 함수 사용

Function.prototype.bind

bind() 메소드가 호출되면 새로운 함수를 생성합니다. 받게되는 첫 인자의 value로는 this 키워드를 설정하고, 이어지는 인자들은 바인드된 함수의 인수에 제공됩니다.

bind 메서드는 this로 사용할 객체를 전달할 수 있습니다.그러므로 bind 메서드는 메서드의 this와 콜백 함수의 this가 불일치하는 문제를 해결하기 위해 유용하게 사용됩니다.

따라서 위에서 보았던 로그인 API를 관리하는 AuthService class에서 bind 메서드를 통해 this를 인스턴스로 가르켜주게 하면 됩니다. bind 메서드를 사용할 위치는 constructor에서 해주면 될 것 같네요.

import {authStorage} from "../../helper/auth/authStorage.ts";
import {AUTH} from "../domainPath.ts";
import networkInstance from "../network.instance.ts";
import {AbstractAuthService} from "./interface.auth.ts";
import {LoginRequest, SignupRequest} from "./types.ts";

// 로그인,회원가입 추상화 인터페이스 구현한 클래스
class AuthService implements AbstractAuthService{

    constructor() {
        
         this.login = this.login.bind(this);
         this.signup = this.signup.bind(this);
    }

    async login(request: LoginRequest): Promise<boolean> {
        const response = await networkInstance(`${AUTH}/login`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
        })

                console.log("this : ", this)


       return  await this.afterAuthAction(response)
    }

...

    private async afterAuthAction(response:Response): Promise<boolean> {
        const result = await response.json();
        if (!response.ok) {
            console.error(result.details);
            throw new Error(result.details);
        }
        authStorage.setToken(result.token);
        return true;
    }
}

export const authService = new AuthService();


constructor 에서 login 메서드를 bind를 통해 this 지정을 해줍니다. 그리고 나서 다른 코드는 건드릴 필요없이 다시 한번 로그인 버튼을 눌러 로그인 API를 태우면 어떻게 되는지 볼까요?

로그인 메서드의 console에서 조회한 this가 AuthService의 인스턴스로 조회되고 로그인 API도 성공적으로 되는 것을 확인할 수 있습니다.

화살표 함수 사용

또 다른 방법으로써는 class의 메서드를 화살표 함수로 선언해주는 방법입니다.
다음과 같이요.

import {authStorage} from "../../helper/auth/authStorage.ts";
import {AUTH} from "../domainPath.ts";
import networkInstance from "../network.instance.ts";
import {AbstractAuthService} from "./interface.auth.ts";
import {LoginRequest, SignupRequest} from "./types.ts";

// 로그인,회원가입 추상화 인터페이스 구현한 클래스
class AuthService implements AbstractAuthService{

   login = async (request: LoginRequest): Promise<boolean> => {
        const response = await networkInstance(`${AUTH}/login`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
        })

       return  await this.afterAuthAction(response)
   }

위와 같이 login 메서드를 화살표 함수 형태의 문법으로 변경해주면 this의 문제를 해결할 수 있어요. 어떻게 화살표 함수로만 변경했는데 문제를 해결한 것일까요?

이는 화살표 함수에 대해서 알아봐야해요.

화살표 함수에서의 this

화살표 함수의 this는 일반 함수의 this와는 다르게 동작합니다.

화살표 함수는 함수 자체의 this 바인딩을 갖지 않습니다. 따라서 화살표 함수 내부의 this를 참조하면 상위 스코프의 this를 그대로 참조하게 됩니다.

따라서 class 메서드를 화살표 함수를 사용하게 되고 해당 함수에서 this를 참조한다면 상위 스코프인 AuthService의 인스턴스를 가리키게 되는 것이죠.

class 메서드에서 this는 인스턴스를 가리킵니다.

화살표 함수의 설계 의도에는 콜백 함수 내부의 this가 외부 함수의 this와 다르기 때문에 발생하는 문제를 해결하기 위함도 포함되어있다고 합니다.

profile
FE DEVELOPER

0개의 댓글