아마도 가장 간단한 Global State (as React)

baealex

소비적인 일보단 생산적인 일을 좋아합니다.

Sign in to view email

Global State

사용자가 로그인이 되었는지 확인하는 상태를 공유할 필요가 있었는데 늘 그렇듯이 props로 전달하려 하였으나 사용중인 Next 상에서 가장 먼저 호출되는 _app.js에선 state를 사용할 수 없었기에(?) 새로운 방법을 찾아야 했다. 진짜 간단한 방법은 쿠키를 사용하는 것이었다. 로그인하면 쿠키에 사용자 이름이라도 새겨놓는 방식이다.

하지만 쿠키는 사용자가 개발자 콘솔로도 생성할 수 있으므로 너무 없어보이는 사이트로 전락하게 된다. 떠오른 다른 방법으론 싱글톤의 클래스를 생성하여 컴포넌트 여기저기서 값을 함께 사용하는 방법이었다.

class Global {
    constructor() {
        this.isLogin = false;
    }

    setLogin(trueOrFalse) {
        this.isLogin = trueOrFalse;
    }

    getLogin() {
        return this.isLogin;
    }
}

export default new Global()

정말 안타까운 점은 한 컴포넌트에서 이 클래스의 멤버 변수의 값을 변경해도 다른 컴포넌트에 실시간으로 갱신된 값을 불러오지 못한다는 점이다. 글로벌 변수로는 사용할 수 있지만 글로벌 State로는 활용할 수 없다. 최근 리액트 자체에서 Context라는 기능이 추가된 것 같은데 뭔가 불필요한 코드를 많이 작성해야 하는 느낌이라 별로였다.

결과적으론 정말 끝끝내 익히고 싶지 않았던 Redux와 같은 라이브러리를 익혀야 할 순간이 왔다고 느꼈다. 언제까지나 props로 연명할 생각은 없었지만 아무리 생각해도 Redux 코드가 도통 이해가 안되서 익힐 엄두가 안났다... 관련 자료를 찾아보던 중 우아한 형제들의 글을 발견했다.


MobX

우아한 형제들 글을 통해서 살펴본 MobX는 매우 훌륭했다. 구조도 간단했고 추가되는 코드도 파이썬의 데코레이션과 유사한 형태로 합리적인 수준이었다. 적용을 위한 튜토리얼 글을 살펴봤는데 이 글이 구조를 살펴보기에 가장 좋았다.

순수 리액트 컴포넌트의 모습, MobX가 적용된 컴포넌트의 모습, MobX와 분리된 컴포넌트 모습을 비롯해 데코데이터(@)를 사용했을때와 안했을때의 모든 형태을 나열하고 있어 매우 인상적이었다.

Next에서 사용하기 위해서 위 글을 참고하며 진행했는데 금방 적용해서 샤샤삭 쓸 것을 기대했는데 생각보다 적용하기가 쉽지가 않았다. 프론트엔드 지식이 전무하다보니 적용도 어렵고 이래저래 화가나기 시작했다.



이를 타개하기 위해선 다른 방법을 고안하거나 꿋꿋이 MobX를 붙잡아야 했는데 문득 class Global이 눈에 아른아른 거렸다. "아... 이거 좀 만 더 만져볼까" 일단 이 친구의 문제를 되짚어보면 변수의 내용을 공유할 순 있지만 변수의 내용이 변경되었을 때 동기화가 안된다는 것이었다.

그렇다면 다른 컴포넌트에서 자신의 setState를 넘겨주고 Global에서 내용이 변경될 때 각각의 컴포넌트의 setState를 호출하면 문제가 해결되지 않을까 싶었다. 솔직히 매우 단순하고 간단한 방법 아닐까? 우선 순수 자바스크립트로 해당 기능을 테스트하였다.

class Global {
    constructor() {
        this.state = {
            shareVal: 'Hello'
        }
        this._updater = []
    }

    setState(newState) {
        this.state = newState
        this._updater.forEach(fn => fn())
    }

    appendUpdater(fn) {
        this._updater.push(fn)
    }
}

const global = new Global()

class A {
    constructor() {
        this.shareVal = global.state.shareVal
        global.appendUpdater(() => this.shareVal = global.state.shareVal)
    }
}

const a = new A()

class B {
    constructor() {
        this.shareVal = global.state.shareVal
        global.appendUpdater(() => this.shareVal = global.state.shareVal)
    }

    changeGlobal() {
        global.setState({
            shareVal: 'Hello World'
        })
    }
}

const b = new B()

console.log(b.shareVal)
b.changeGlobal()
console.log(a.shareVal)
Hello
Hello World

결과는 매우 만족스러웠으며 매우 단순한 구조인데 걱정스러운 부분은 React에서도 의도대로 동작해줄런지... 로그인 상태를 공유해야 하는 곳은 상단 네비게이션과 포스트 본문 내부였기에 각각의 컴포넌트의 생성자에 위 처럼 내용을 각각 추가하였다.

class TopNavigation extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            onNav: false,
            isLogin: Global.state.isLogin
        };
        Global.appendUpdater(() => this.setState({
            ...this.state,
            isLogin: Global.state.isLogin
        }));
    }

    componentDidMount() {
        const alive = await API.alive();
        Global.setState({
            isLogin: alive.data !== 'dead' ? true : false
        });
    }
}
class Post extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isLogin: Global.state.isLogin,
            isLiked: props.post.is_liked === 'true' ? true : false,
            totalLikes: props.post.total_likes
        }
        Global.appendUpdater(() => this.setState({
            ...this.state,
            isLogin: Global.state.isLogin
        }));
    }
}

이론상 되야했는데 진짜 되더라. 성능은 모르겠다. 나머지는 React에게 맡긴다...

상단 네비게이션이 마운트되면서 로그인 상태를 확인하고 글로벌 변수를 업데이트하고 포스트 내부에서 로그인 상태인지 확인하였는데 일단 의도하는 대로 동작된다. 정말 좋은게 추가되는 코드나 의존적인 코드가 매우 적다는 것이었다! "나 이 값 필요해!" 그럼 가져만 오면 된다. "동기화가 필요해!" 그럼 setState를 던져주면 된다. 왜 이생각을 여지껏 못한걸까.



다만 성능에 대해선 글쎄? 이렇게 처리하면 여러 컴포넌트의 setState를 지속적으로 요청되므로 불필요한 연산을 하게되는 셈이며 이는 전적으로 리액트가 처리하므로 내부 동작을 모르는 나는 막연한 불안감이 안아야 한다. 또한 컴포넌트가 불러올때마다 Updater에 익명 함수가 들어가므로 결과적으론 Updater엔 쓰레기 함수가 쌓이고 머무는 시간이 늘어날 수록 호출 해야하는 Updater의 수가 많아 진다. 이걸 없앨 수 있는 방법을 찾아야 했다.

class Global {
    constructor() {
        this.state = {
            shareVal: ''
        }
        this._updater = {};
    }

    setState(newState) {
        this.state = newState;
        Object.keys(this._updater).forEach(key => {
            try {
                this._updater[key]();
            } catch(e) {
                delete this._updater[key];
            }
        });
    }

    appendUpdater(name, fn) {
        this._updater[name] = fn;
    }

    popUpdater(name) {
        delete this._updater[name];
    }
}

export default new Global();

처음에 괜히 Updater를 List로 접근해서 별생각 별짓 다했는데 그냥 Updater를 Object로 만들어두고 컴포넌트의 이름과 익명함수를 같이 던져주면 되겠구나. 컴포넌트 언마운트 되는 시점엔 해당 컴포넌트의 함수를 삭제해주면 간단해진다.

아 진짜 이제 프론트엔드 개발하면서 걸림돌이 되던 애들은 다 끝냈으니 단순 작업을 빠르게 진행할 수 있을 것 같다... 휴...

작성된 댓글이 없습니다!
로그인된 사용자만 댓글을 작성할 수 있습니다.