Graphql Subscription 과 Hook 을 활용한 반응형 페이지 개발

(Server 사이드) SubscriptionServer 구현

  • graphql Server 생성시, subscriptionEndpoint 정의 (해당 endpoint 는 향후, client 사이드의 뷰 페이지에서 세션 생성을 위한 enpoint 주소로 사용됨)

  • subscription-transport-ws 모듈을 활용한 SubscriptionServer 생성

  • SubscriptionServer 생성시, schema 와 websocket Endpoint 정의

const { SubscriptionServer } = require('subscriptions-transport-ws');
const { execute, subscribe } = require('graphql');

app.use('/graphql', graphqlHTTP({
 schema: graphqlSchema,
 graphiql: { subscriptionEndpoint: `ws://172.31.34.34:${PORT}/subscriptions` },
 context: { startTime: Date.now() },
 extensions,
 customFormatErrorFn: (error) => {
 logger.error('[GraphQL] ' + error.message );
 return({
 message: error.message || 'An error occurred.',
 locations: error.locations,
 stack: error.stack ? error.stack.split('\n') : []
    });    
  }
}));

const ws = http.createServer(app);

ws.listen(parseInt(PORT)+1, '172.31.34.34', () => {
 console.log(`Apollo server is now running on http://localhost:`+String(parseInt(PORT)+1));
 new SubscriptionServer(
    {
 execute,
 subscribe,
 schema: graphqlSchema,
    },
    {
 server: ws,
 path: '/subscriptions',
    },
  );
});

app.listen(PORT, '172.31.34.34', () => {
 console.log(`HTTP Server listening on port :${PORT}/`);
})


여기서 유념해야 하는 점은, grapqh 서버와 web socket 서버를 분리해서 정의하고, graphql 서버 생성시 websocket endpoint 를 정의해 주어야 하는 것이다.


(Server 사이드) PubSub 구현

  • PubSub 글로벌 인스턴스 생성

const { PubSub } = require('graphql-subscriptions');
const clients = {};

module.exports.init = () => {
 const pubsubInstance = new PubSub();
 clients.pubsubInstance = pubsubInstance;
};

module.exports.getClients = () => clients;



Server 사이드의 복수개의 서비스들이 필요시 마다 publish 를 수행할 수 있도록, 글로벌 인스턴스를 선언하여 다양한 서비스들이 이를 통해 publish 할 수 있도록 함


(Server 사이드) subscription schema 정의

  • graphql subscription schema 정의

type AlarmTopic {
 rule_id: String,
 rule_name: String,
 timestamp: String,
 message: String,
 level: String,
 state: String,
 failover_id: String
}

  • graphql subscription resolver 정의

const pubsubClient = pubsub.getClients().pubsubInstance;
schemaComposer.Subscription.addFields({
 subscribeAlarm: {
 type: 'AlarmTopic',
 subscribe: withFilter(
      () => pubsubClient.asyncIterator(PUBSUB_TOPICS.ADD_ALARM),
      () => {
 return true;
      }
    )
  },
});

여기서 주요한 점은 graphql subscription resolver 구현시, 글로벌 pubsub 인스턴스를 활용하여 pubsubClient.asyncIterator(PUBSUB_TOPICS.ADD_ALARM) 를 통해 subscription 을 선언하는 점이다. 그리고 이때 PUBSUB_TOPICS.ADD_ALARM 을 subscription 을 위한 topic 이름으로써, subsciption resolver 는 해당 topic 으로 publish 하는 메시지 마다 graphql subsciption 을 요청한 Client 모듈로 해당 메시지가 전달되게 된다.


(Server 사이드) 서비스에서 publish 수행

  • 글로벌 pubsub 인스턴스를 통한 메시지 push

// MongoDB service add
 dbService.esaAddAlarm(rule_id, rule_name, timestamp, message, data, level, state, failover_id, req)
      .then(r => {
 const pubsubClient = pubsub.getClients().pubsubInstance;
 const pubData = {
 rule_id: rule_id,
 rule_name: rule_name,
 timestamp: timestamp,
 message: message,
 level: level,
 state: state,
 failover_id: failover_id
        }

 pubsubClient.publish(
 PUBSUB_TOPICS.ADD_ALARM, 
          { subscribeAlarm: pubData }
        );

 res.send(r);
      })
      .catch(next);
  });

상기 예는 서버 사이드에서 신규 DB 엔트리 추가시 마다, 해당 데이터를 publish 하여 graphql subscription 의 client 측으로 데이터를 전달하는 코드의 예이다. pubsubClient.publish() 를 통해 전달된 데이터는 client 측에서는 다음과 같은 형태로 데이터가 전달된다.

{
    subscribeAlarm: {
        subscribeAlarm: {
             rule_id
             rule_name
             timestamp
             message
             level
             state
             failover_id
        }
    }
}

(Client 사이드) subscription 을 위한 GraphGLClient 정의

  • GraphQLClient 정의

  • ClientContext.Provider 를 통한 App Wrapping

const client = new GraphQLClient({
 url: 'http://'+process.env.REACT_APP_GQL_SERVER+':'+process.env.REACT_APP_GQL_PORT+'/graphql',
 cache: memCache(),
 subscriptionClient: new SubscriptionClient('ws://'+process.env.REACT_APP_GQL_SERVER+':'+process.env.REACT_APP_WS_PORT+'/subscriptions', {
 reconnect: true
  })
})

function App() {
 useStyles();

 const { settings } = useSettings();

 return (
 <ClientContext.Provider value={client}>
 <ThemeProvider theme={createTheme(settings)}>
 <StylesProvider jss={jss}>
 <MuiPickersUtilsProvider utils={MomentUtils}>
 <SnackbarProvider maxSnack={1}>
 <Router history={history}>
 <Auth>
 <ScrollReset />
 <GoogleAnalytics />
 <CookiesNotification />
 <SettingsNotification />
 <Routes />
 </Auth>
 </Router>
 </SnackbarProvider>
 </MuiPickersUtilsProvider>
 </StylesProvider>
 </ThemeProvider>
 </ClientContext.Provider>
  );
}



GraphQLClient 의 subscriptionClient 옵션을 통해 Server 사이드의 web socket 서버와 session 을 형성하게됨. 이때 참조되는 주소는 Server 사이드의 subscriptionEndpoint 값과 동일해야 함.


(Client 사이드) useSubscription() 및 callback 함수 정의

  • useSubscription() 선언

  • callback() 함수 정의

import { useSubscription } from "graphql-hooks";

const ALARM_SUBSCRIPTION = `
  subscription OnAlarmAdd {
    subscribeAlarm {
      rule_id
      rule_name
      timestamp
      message
      level
      state
      failover_id
    }
  }
`

 useSubscription({ query: ALARM_SUBSCRIPTION }, ({ data, errors }) => {
 if (errors && errors.length > 0) {
 setError(errors[0])
 return
    }   
 // Call Back function 정의
  })

// subscription response data form
data = {
    subscribeAlarm: {
        subscribeAlarm: {
             rule_id
             rule_name
             timestamp
             message
             level
             state
             failover_id
        }
    }
}