Giter Site home page Giter Site logo

snack-game / server Goto Github PK

View Code? Open in Web Editor NEW
6.0 1.0 0.0 665 KB

스낵게임 서버 어플리케이션. 게임 검증과 리더보드, 회원 관리 서비스를 담당합니다

Java 54.67% Kotlin 45.33%
java game spring spring-boot spring-data-jpa

server's Introduction

게임 서버

API 설계

도메인 분석
사용 시나리오 in Figma

  • 많은 동시접속자를 소화해야 한다 (1000명 이상, 아직 정확한 바는 없음)
    • 이를 위해 스케일 아웃을 할 가능성이 아주 높다
    • 따라서 서버 간 인증 정보 동기화에 대한 비용을 줄이고자 JWT를 채택한다

인증

기능

  • 토큰에서 사용자를 식별한다.

API

  • 닉네임으로 토큰을 발급한다.

사용자

  • 이름, 그룹은 없을 수 있다.

기능

  • 임시 사용자를 생성한다.
    • 이름을 무작위 생성한다.
  • 이름, 그룹이름으로 사용자를 생성한다.
    • 이름은 중복될 수 없다.
  • 이름을 수정한다.
    • 이름은 중복될 수 없다.
  • 그룹을 수정한다.

API

  • 닉네임으로 사용자를 생성한다.
    • 사용자 정보와 토큰을 반환한다.
  • 임시 사용자를 생성한다.
    • 사용자 정보와 토큰을 반환한다.
  • 이름을 수정한다.
  • 그룹을 수정한다.
  • 자신의 정보를 알 수 있다.
  • 특정 이름으로 시작하는 그룹 이름을 검색한다
  • 특정 이름으로 시작하는 사용자 이름을 검색한다

랭킹

기능

  • 전체 랭킹을 조회한다.
    • 베스트 점수가 기준이다.
    • 50등까지 조회한다.
    • 내 랭킹(전체)을 포함한다.
  • 그룹 내 랭킹을 조회한다.
  • 특정 이름으로 시작하는 사용자들의 랭킹을 조회한다.

사과 게임

도메인 기능

  • 게임
    • 10*18 사이즈의 게임판을 생성한다.
    • 초기화한다
      • 게임판을 재생성한다.
      • 게임 시간을 초기화한다.
      • 점수를 초기화한다
    • 특정 범위의 사과들을 제거한다
      • 제거 개수만큼 점수를 얻는다
    • 생성된지 120초가 지나면 더 이상 조작할 수 없다.
    • 게임 주인인지 알 수 있다
  • 게임판
    • 원하는 크기의 게임판을 랜덤 생성한다.
    • 특정 범위의 사과들을 제거한다
      • 범위가 게임판과 일치하는지 검증한다
        • 사과 좌표들에 사과가 없으면 예외
        • 사과 좌표들 외에 사과가 있으면 예외
      • 사과들의 합이 10이어야 제거할 수 있다
      • 제거된 개수를 반환한다
  • 범위
    • 좌상단을 알 수 있다
    • 우하단을 알 수 있다
    • 범위 내의 모든 좌표들을 알 수 있다
    • 사과 좌표들을 알 수 있다
    • 빈 좌표들을 알 수 있다
  • 좌표
    • 음수이면 예외를 던진다
  • 사과
    • 숫자는 1~9 사이이다.
    • 랜덤 숫자로 생성할 수 있다.
    • 빈 사과인지 알 수 있다

server's People

Contributors

0chil avatar hwanvely avatar

Stargazers

김지환 avatar  avatar YoonJuHo avatar 오진호 avatar  avatar  avatar

Watchers

Hee Su avatar

server's Issues

최고 점수로 랭킹을 운영한다

최고 점수를 기준으로 랭킹을 운영한다

  • 랭킹 페이지 사용자 중복을 막는다
  • 잘하는 사람이 랭킹페이지의 절반을 독식하게됨
  • 유저에게 제공하는 컨텐츠의 질이 떨어짐

기대 효과

  • 랭킹페이지 컨텐츠의 질이 올라감
  • 랭킹 페이지 독식을 막아 새로운 사용자의 유입을 촉진함

사용자의 게임 전적을 조회한다

마이페이지에서 사용자의 게임전적을 보여주려고 한다.

현재는 두가지 방식을 보여줄 것이다.

  1. 최근 25게임의 점수
  2. 최근 1주일간 최고점수

게임 생성 시 종료시각을 함께 지정한다

관련 이슈

AS-IS

  • 게임 종료 시 직접 DB를 update하여 게임을 종료한다.

이 방식은 완료된 게임을 직접 끝내야 하기 때문에 누락의 가능성이 있으며, 실제로 완료되지 않은 게임이 DB에 점점 쌓이고 있다.

TO-BE

  • 게임 생성 시 종료시각을 지정한다.

게임의 종료 시기는 게임 시작부터 알 수 있다.
위 문제를 해결하기 위해 스케쥴러를 통한 주기적인 '청소'를 수행하는 방법도 있으나, 마찬가지로 수동적이기 때문에 수행되지 않을 일말의 가능성이 있다.
따라서 게임 생성 시 이 정보를 미리 삽입하여 해결한다.
'이미 끝난 게임'을 만들 수 있어 테스트가 간단해지는 장점도 있다.

사용자의 이름을 랜덤 생성한다 V2

AS-IS

앱 런칭을 준비하면서 Apple 소셜로그인이 생기기 전까지 카카오와 구글을 통한 소셜로그인은 사용자의 소셜 계정 이름을 받아와 생성하였습니다.
또한 게스트의 경우 알파벳을 이용하여 랜덤 생성을 하였습니다.

Apple 소셜 로그인을 도입한 후 사용자의 계정 이름 정보를 제공해 주지 않아 사용자의 이름을 게스트와 같이 랜덤 생성하였고 그 결과 랭킹 페이지에서 레벨이 밀리는 문제가 발생하였습니다.
또한 랜덤으로 나열된 알파벳을 가진 이름이 랭킹 페이지에 보여지는 것은 좋지 않다고 판단하였습니다.


TO-BE

따라서

  1. 이름의 최대 길이를 제한하여 문제를 예방하고
    #154
  2. 우리말을 조합한 이름을 랜덤 생성하여 사용자의 경험을 향상시키려 합니다.
    i. 부사 + 형용사 + "스낵이" 의 형태

랭킹 시스템 개발

랭킹

  • 베스트 점수를 보여준다
  • 50개
  • 그룹별, 사용자별

✅ 랭킹 가져오기

REQUEST
GET /rankings
Authorization: "Bearer XXX"
RESPONSE
body:
{
	memberGroup: {
		rankings: [
			{
				name: "똥수",
				score: 123,
				ranking: 1
			},
			{
				name: "땡칠",
				score: 122,
				ranking: 2
			}
		],
		my: {
			name: "똥수",
			score: 123,
			ranking: 1
		}
	},
	all: {
		rankings: [
			{
				name: "똥수",
				score: 123,
				ranking: 1
			},
			{
				name: "땡칠",
				score: 122,
				ranking: 2
			}
		],
		my: {
			name: "똥수",
			score: 123,
			ranking: 1
		}
	}
}

랭킹을 닉네임으로 검색

REQUEST
GET /rankings?startWith="똥"
Authorization: "Bearer XXX"
RESPONSE
body:
{
	memberGroup: {
		rankings: [
			{
				name: "똥수",
				score: 123,
				ranking: 1
			},
			{
				name: "똥쟁이",
				score: 122,
				ranking: 2
			}
		]
	},
	all: {
		rankings: [
			{
				name: "똥수",
				score: 123,
				ranking: 1
			},
			{
				name: "땡칠",
				score: 122,
				ranking: 2
			}
		]
	}
}

세션 저장소와 어플리케이션의 생명 주기를 분리한다

AS-IS

현재 인-메모리 세션 저장소를 사용중이다.
때문에 어플리케이션 재시작 시 세션이 모두 증발하는 문제가 있으며,
세션을 활용하는 일부 기능에서 가끔 장애를 겪을 수 있다.

TO-BE

MYSQL DB나 별도의 메모리 DB를 고민하여 세션 저장소를 적절한 곳으로 분리 조치한다.

세션을 시뮬레이션 한다

AS-IS

스낵게임 재작성 및 조작 방식 변경 이후 세션을 시뮬레이션하여 검증 할 수 있는 방법이 없었습니다.

TO-BE

게임이 끝난후 시뮬레이션을 통해 세션을 검증할 수 있습니다.

구현 목록

  • 게임
    • 게임을 시작한다.
      • 랜덤 생성된 게임판 반환
    • 게임을 일시정지한다.
    • 게임을 재개한다.
    • 게임을 종료한다.
      • 게임을 시뮬레이션한다.
      • 획득한 점수만큼 사용자의 경험치를 올린다.
    • 스낵을 제거할 수 있다.
      • 스낵을 제거한 수만큼 점수가 증가한다.
      • 황금사과를 제거하면
        • 게임판을 초기화한다.
    • 점수를 계산할 수 있다.
    • 사용자를 식별할 수 있다.
    • 시간이 만료되면 움질일 수 없다.
  • 게임판
    • 게임판을 랜덤 생성한다.
      • 게임판의 크기는 6 * 8 이다.
      • 하나의 황금 사과를 반드시 포함한다.
    • 게임판을 리셋한다.
    • 스낵을 제거할 수 있다.
      • 스낵의 합이 10이어야 제거 가능하다.
  • 좌표
    • 양수여야한다
  • 스트릭(Streak)
    • 스트릭은 상하좌우대각으로 연속되어야한다.
  • 예외처리 재작성
  • Apple -> Snack 재작성

AppleGame의 점수를 계산 방식을 개선한다.

현재 테스트의 편의를 위하여 AppleGame 생성과 동시에 score를 지정할 수 있도록 해두었다.
이는 public으로 열려있어 좋지 않으므로 개선할 여지를 갖고 있다.

따라서 score를 계산하는 방식 자체를 개선하여 문제를 해결하려고 한다.

Spring Data JPA 도입

Spring Data JPA를 도입한다

  • DB를 다루는 부분을 최대한 JPA에 위임한다.
  • 객체지향 세계와 DB 사이의 패러다임 불일치를 최대한 완화한다.
  • JdbcTemplate을 사용해 직접 다루는 것보다 성능적 향상을 기대할 수 있다. (1차 캐시)
    • DB에 접속하거나 SQL을 전송하는 것은 가장 큰 오버헤드이므로, 이를 줄이면 성능적 향상이 있지 않을까? (가설)
  • 생산성 증대를 기대할 수 있다.
    • JPA가 짜주는 쿼리에 대해서는 테스트를 하지 않아도 된다.
    • 혼자 개발하고 있으므로 생산성을 향상할 수단도 필요하다.

연혁 만들기

언제 시작해서, 지금까지 무엇을 했고, 어떤 변화들이 있었는지 연혁을 작성한다

게스트를 회원으로 전환한다

개요

사용자가 적절한 시점에 게스트를 회원으로 전환할 수 있다.

기능

소셜 계정을 연결할 수 있게 한다.
소셜 계정이 연결되면 게스트는 회원으로 전환된다.

목적

  • 회원 전환률 증가

  • 사용자 소유욕구 충족

전환된 계정은 소유의 개념이 생기므로 방금 만든 기록도 소유하게 된다.
오락실에서 1등에 이름새기는 것과 비슷한 개념이다.

의존성 사이클을 제거한다

재미있게도 현재 의존성 사이클이 존재한다.
image
이 부분들을 개선해보자. 이유는 대부분 dto에 있을 것으로 보인다.


마지막 미션에서 느낀 개선 사항

지금까지 연관관계 매핑으로 직관성만을 추구했다. 하지만 극단적이면 탈이 나는 법.
조영호님이 '연관관계 매핑은 DB단에서 어떻게 돌아가는지를 잊게 만든다' 라고 하셨다.
나도 잠시 이 부분을 망각하고 있었다.
실제로 객체를 표현할 땐 연관관계 매핑을 통해 DB를 거의 신경쓰지 않고 구현할 수 있었기 때문이다.

기술이란건 트레이드 오프를 완화해주는 것이다.
하지만 나는 트레이드 오프를 고려하지 않고 너무 도메인 표현에만 집중한 것 같다. (객체 그래프 탐색으로 다 하고 싶어함)
어디까지 결합을 시켜서 -> 객체 그래프 탐색으로 접근 가능하게 할지에 대한 기준이 없었다.

아직 비즈니스 자체가 작아서 그렇지, 좀만 더 커지면 서로 얽혀버려서 떼어내기가 보통 일이 아닐 것이다.

의존성에 대해
Assocation은 객체간의 결합도가 아주 높아지고, 불필요한 부분까지 객체 그래프 탐색을 통해 접근할 수 있게 하는 문제가 있다. + 의존성 사이클이 생기면 나중에 변경이 어렵다.
따라서 서로 생명주기가 너무 다른 컨텍스트 간에는 Id로 간접 참조를 고려해보자.
그리고 분리해낼 수 있는건 또 분리해보자 (인증 부분)

개선 가능한 부분

닉네임 결정하는 부분은 사실 DB 탐색이 필요한 도메인이다.
그래서 서비스에 놔두기도, 도메인에 놔두기도 애매했다.
그러나 이 부분을 멤버에 관련된 또 하나의 도메인으로 분리해 볼 수 있겠다는 생각이 들었다.
(의존성이나 계층을 망가뜨리지 않으면서)

이름 중복

(그래선 안되지만) 이름이 중복될 수 있다.
실제 중복된 이름의 사용자가 추가되기 전에 보완이 필요하다.

관련해 파트너와 논의를 해봤고 결론은 다음과 같다:

'희수'라는 사용자가 이미 존재할 경우
'희수_2' 이름으로 만들자
'희수_2'도 존재할 경우
'희수_3' 이름으로 만들자

현재 존재하는 SPOF를 해결한다

AS-IS

운영 환경 기준 서버의 모든 자원은 한개씩만 존재하였습니다.
이런 이유로 아래의 세가지 부분이 SPOF가 될 것이라 예상하고 해결하려합니다.

  1. EC2
  2. DB
  3. S3

TO-BE

  • EC2 이중화
  • 로그인 방식 변경
  • DB 이중화
  • MySQL Replication
  • S3
  • 어플리케이션을 활용한 fallback 방식

랭킹 조회시 게임 개수만큼 쿼리가 발생하는 문제

관련된 PR

고민

다시 든 고민

정확도나 의존성에 관련한 고민은 어느정도 결론을 내렸다.

하지만 성능상의 문제가 남아있다.

바로 게임 개수만큼 쿼리가 날아가는 것. 😅🤮

다음과 같은 코드 때문이다:

image image

결과는 다음과 같다:

한 페이지 넘길 때마다 쿼리가 50개 발생한다?? 이건 사고다.

하지만 방법은 있다. 충분히 한방에 받아올 수 있긴 하다.

List<Long> sessionIds = top50Rankings.stream()
                .map(RankingDto::getSessionId)
                .collect(Collectors.toList());
Map<Long, AppleGame> sessions = appleGameSessions.findAllById(sessionIds).stream()
        .collect(Collectors.toMap(AppleGame::getSessionId, appleGame -> appleGame));
// 세션들을 한방에 받아와 해싱해둔다.

sessions.get(1L);
sessions.get(2L);
sessions.get(3L);
sessions.get(4L);

하지만 서비스 로직이 복잡해진다🥲

어떻게 하면 성능을 잡으면서 로직도 명료하게 유지할 수 있을까?

QueryDSL을 도입한다

복잡한 쿼리를 텍스트 대신 코드로 작성하여 관리한다.
JOOQ 사용은 어떤지 고민해보자.
image

토큰의 저장소로써 쿠키를 사용하는 것을 재고해본다.

AS-IS
현재 엑세스 토큰과 리프레시 토큰 모두 쿠키에 싣어 서버에서 관리하고 있다.
토큰이 만료되면 브라우저가 자동으로 쿠키에서 만료된 토큰을 삭제한다.
이로인해 서버에서는 만료 여부를 검증 할 수 없고 클라이언트도 그에 대응하는 응답을 받지 못한다.
노션 페이지에 적어두었습니다.

TO-BE
엑세스 토큰을 다시 헤더에 싣거나 세션을 고려해봐야 한다.

회원탈퇴를 할 수 있다

  • snack-game/front#279
    에서 논의한 사항을 반영해 회원탈퇴를 구현한다.

  • 탈퇴한 소셜 계정으로 재가입 할 수 있다.

  • 회원을 탈퇴하는 즉시 전적 및 랭킹, 경험치가 제거된다

집계 객체에 대한 동시성 문제를 해결한다

image

스낵게임의 랭킹 시스템은 집계 객체(혹은 테이블)로 운영된다. (그 이유는 이곳을 참고할 수 있다)
그러나 현재 집계 객체의 생성, 갱신 과정에 동시성 문제가 있다.
상황에 따라 2개 이상의 집계 객체가 생성되거나 'Second Lost Update' 문제가 발생할 수 있다.

비즈니스 정책부터, DB 격리 수준, 락킹 메커니즘까지 여러 선택지를 고려해 이 문제를 해결해보자.

기본 데이터가 입력되지 않는 문제

기본 데이터(data.sql)이 입력되지 않는다.
원인은 다음과 같다:

  • spring.jpa.hibernate.ddl-auto=create로 인해 table이 drop 되는 점
  • data.sql의 사용 시점이 table drop 이전인 점

이름 제약사항을 추가한다

닉네임과 그룹은 특수문자를 제외한 완성된 글자(ㅇㅇ, ㅇ가 안됨)와 숫자의 조합으로 길이 2이상을 만족해야 함.

  • 닉네임
    • 한글, 영어만 허용한다.
      • 한글은 완성된 글자여야 한다. (ㅇㅇ, ㅇ 불가)
    • 길이가 2 이상이다.
  • 그룹
    • 아예 없을 수 있다.
    • 한글, 영어만 허용한다.
      • 한글은 완성된 글자여야 한다. (ㅇㅇ, ㅇ 불가)
    • 길이가 2 이상이다.

레이턴시 문제를 해결한다

정리 자료

서버가 Idle인 상태에서 첫 요청을 받을 때, 평균보다 훨씬 큰 latency가 발생한다.
쓰레드풀, GC, JIT, 웜업, DB 커넥션 풀에 문제가 있는지 살펴보았고, 문제가 없어 서버 마이그레이션으로 환경도 바꿔보았다.
레이턴시가 개선되긴 했으나, 증상 자체가 사라지지는 않았다.

추가로 DB 버퍼 풀의 크기도 늘려보았지만, 개선이 거의 없었다.
도대체 무엇이 문제인 것인지 잘 모르겠다.
조만간 쿼리를 개선해야하므로 그 때 다시 시도해보도록 한다.

사과게임 시스템 개발

  • 게임을 시작하고, 게임판을 생성한다.
  • 중간중간 진행 상황을 입력받고, 검증해 저장한다.
  • 게임이 끝나면 결과를 저장한다.

'레벨' 구조에 맞춰 DDL을 변경한다

DDL 문제로 배포에 실패했습니다 :(
다행히 어플리케이션이 바로 롤백 되었지만, 배포하려면 DDL 작업이 우선되어야 하겠네요.
내일 제가 작업해놓겠습니당

API 요청 주소 수정

CORS 관련 문제를 해결한다.
운영 도메인과 개발 도메인에 대해 CORS를 허용한다.
모든 Origin을 허용하는 기존 정책을 유지한다.

  • 개발 단계에서 localhost, vercel 임시 도메인, 운영 도메인 등 어떤 도메인으로 접속할지 한정하기 어렵다.

API 문서의 요청 주소를 /으로 수정한다.

기존 '세션에 수 삽입' API를 제거한다

기존에 사용하던 수 삽입을 새 클라이언트 배포가 완료되는 대로 제거한다.
하위 호환성을 유지하기 위해 코드를 이중으로 유지하고 있어 복잡성이 조금 있는 상태이다.

각종 성능을 테스트 및 비교한다

  1. 각종 성능 지표를 테스트 한 후 비교하는 자료를 남긴다.
  • 구 랭킹(Deprecated) vs 현 랭킹 (집계 객체 여부에 따른 조회 성능 차이)
  • 사과 객체 캐싱 vs 캐싱 X (JVM 최적화는 어디까지 해주는가?)
  • Java 11 vs Java 17 (버전을 올린 후 성능 개선이 있는지 테스트)

배포 자동화를 무중단으로 개선한다

AS-IS

배포시 10초 이내의 다운타임이 있으며, 이에 대한 별도의 클라이언트 예외처리는 없다.

TO-BE

nginx 리버스 프록시를 활용해 다운타임을 제거한다.
RELOAD 하더라도 기존 요청이 끝날 때까지 연결을 유지하는 worker의 특성을 활용한다.

테스트를 보완한다

의존성을 약하게 관리하기 위해 이벤트 기반 구조를 일부 도입했다.
이에 맞게 이벤트 기반 구조를 고려해 서비스 테스트 및 통합 테스트를 추가한다.

이름으로 로그인

이름으로 로그인하여 토큰을 받을 수 있게 한다.
(아직) 비밀번호는 없다.

그룹 없이 사용자를 생성할 수 있다.

AS-IS

도메인 코드 상에서는 가능했지만, 테스트의 부재로 오류를 놓쳐 실제 프로덕트에서 오류가 발생하였다.

TO-BE

이제 그룹 없이 사용자를 생성할 수 있다.

배운 점

도메인이 비즈니스를 충실히 반영한다고 해도, 객체들의 연결이나 입출력이 잘못되면 실제 사용자에게는 의도하지 않은 모습(예외)를 보여줄 수 있다.
따라서 이를 보완할 수 있는 테스트가 필요하다.

이번 일은 기존 프로젝트들에서도 겪었고, 컨트롤러 테스트를 통해 어느정도 보장한다고 생각했다.
그래서 통합 테스트의 필요성을 느끼지 못했다.
하지만 놓치는 부분이 있었다.

나에게 컨트롤러 테스트는 '요청과 응답 형식, 그리고 의도한 핸들러가 호출되는지?' 정도를 테스트하기 위한 것이다.
따라서 사용자 입장에서 XXX를 요청할 때

  • OOO 하면 실패한다.
  • OOO 하면 성공한다.
    와 같이 구체적인 유즈케이스는 놓치곤 하였다.

하지만 실제 프로덕트에서 이러면 사용자가 장애를 겪게 된다.
따라서 '전체 프로덕트가 잘 연결되고, 사용자에게 의도대로 전달되는지?'
즉, 이번 작업의 목적을 달성했는지? 를 보장하기 위해선 도메인과 컨트롤러 테스트만으로는 충분하지 않으며
추가적인 테스트가 필요하다고 생각했다.

결론

코드가 목적을 달성했는지 확인하기 위해선 테스트를 통해 작업의 목적을 밝히고, 검증해야
그 의도가 오래도록 보장될 수 있다고 생각한다.

그래서 도메인 테스트, 서비스 테스트를 구체적으로 작성하는것도 중요하겠지만,
이렇게 작성한 비즈니스가 의도한대로 사용자에게 잘 전달되는지? 를 확인하려면 또 다른 방법이 필요하다.

여기서 작업의 목적을 명확하게 기술하고, 테스트하기 위해서 통합 테스트 작성이 상당히 효율적이고 안전한 방법이라고 생각이 들었다.

앞으로는 목적 달성을 확인하기 위한 통합 테스트도 작성해 보자.
사용자에게 의도한대로 전달만 되면, 그 내부는 언제든 개선할 수 있다.

CD 방식을 변경한다

  • 서버 마이그레이션 상황이 발생
  • AWS S3, CodeDeploy, EC2 에 더 이상 의존할 수 없는 상황
  • Github Actions의 Self-hosted Runner를 활용한다.

중복 이름을 방지한다

이름이 중복될 수 있는 문제가 있다.
이름이 중복되는 경우 '_2', '_3' 등 숫자를 붙여 해결한다.
이는 소셜 로그인에만 적용하며,
사용자가 즉시 오류 복구가 가능한 '일반 사용자 생성', '닉네임 바꾸기'등의 경우에는 예외를 내보낸다.

세션에 여러 개의 수를 삽입한다

클라이언트에서 한번에 여러 개의 수를 삽입할 수 있다.

AS-IS

'한 수'에 해당하는 좌표들만 입력할 수 있었다.
image

TO-BE

여러 수를 입력할 수 있게 된다.
image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.