Skip to content

[2팀 고다솜] Chapter 4-1 성능최적화: SSR, SSG, Infra#30

Open
ds92ko wants to merge 27 commits intohanghae-plus:mainfrom
ds92ko:main
Open

[2팀 고다솜] Chapter 4-1 성능최적화: SSR, SSG, Infra#30
ds92ko wants to merge 27 commits intohanghae-plus:mainfrom
ds92ko:main

Conversation

@ds92ko
Copy link
Member

@ds92ko ds92ko commented Dec 16, 2025

과제 체크포인트

배포 링크

기본과제 (Vanilla SSR & SSG)

Express SSR 서버

  • Express 미들웨어 기반 서버 구현
  • 개발/프로덕션 환경 분기 처리
  • HTML 템플릿 치환 (<!--app-html-->, <!--app-head-->)

서버 사이드 렌더링

  • 서버에서 동작하는 Router 구현
  • 서버 데이터 프리페칭 (상품 목록, 상품 상세)
  • 서버 상태관리 초기화

클라이언트 Hydration

  • window.__INITIAL_DATA__ 스크립트 주입
  • 클라이언트 상태 복원
  • 서버-클라이언트 데이터 일치

Static Site Generation

  • 동적 라우트 SSG (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포

심화과제 (React SSR & SSG)

React SSR

  • renderToString 서버 렌더링
  • TypeScript SSR 모듈 빌드
  • Universal React Router (서버/클라이언트 분기)
  • React 상태관리 서버 초기화

React Hydration

  • Hydration 불일치 방지
  • 클라이언트 상태 복원

Static Site Generation

  • 동적 라우트 SSG (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포

아하! 모먼트 (A-ha! Moment)

하이드레이션: 단순한 "이벤트 연결"을 넘어선 "상태의 복원"

과제를 시작하기 전까지 하이드레이션을 단순히 "서버에서 렌더링된 HTML에 이벤트 리스너를 등록하는 과정" 정도로만 이해하고 있었습니다.
하지만 실제로 구현해보면서, 하이드레이션은 훨씬 더 근본적인 문제를 다루는 메커니즘이라는 점을 깨달았습니다.

과제 시작 전 하이드레이션에 대한 오해

서버가 이미 HTML을 다 그려서 줬는데, 클라이언트에서 왜 다시 데이터를 받아오거나 직렬화/역직렬화가 필요한거지?
그냥 HTML 위에 JS만 얹으면 되는거 아니야?

서버와 클라이언트를 하나의 연속된 환경으로 생각했던 것이 이러한 오해를 일으켰던 것 같습니다.
실제로 서버와 클라이언트는 완전히 분리된 환경이며, 서버에서 생성된 HTML은 특정 상태를 기준으로 이미 렌더링이 완료된 결과물입니다.

만약 서버에서 만든 HTML과 클라이언트가 첫 렌더링 시점에 가질 상태가 다르면, React는 기존 DOM을 신뢰할 수 없다고 판단하고 브라우저는 이미 그려진 HTML을 폐기한 뒤 다시 렌더링하게 되고, 이것이 "Hydration Mismatch"가 발생하는 이유였습니다.

이 과정을 통해 하이드레이션의 본질은 서버와 클라이언트가 동일한 SSOT를 기준으로 렌더링을 이어가도록 강제하는 것임을 깨달았습니다.
이를 위해 서버 상태를 직렬화해 클라이언트에 전달하고, 클라이언트는 이를 다시 역직렬화하여 스토어의 초기 상태로 주입하는 과정이 선행되어야 합니다.


데이터 동기화의 핵심: window.__INITIAL_DATA__의 존재 이유

window.__INITIAL_DATA__가 필요한건데?
어차피 클라이언트에서 필요하면 다시 API fetch하면 되는데, 굳이 초기 데이터를 미리 넘길 필요가 있나?

과제 초기에는 서버에서 내려준 HTML에 이미 데이터가 녹아있는데, 왜 전역 객체에 데이터를 또 중복해서 담는지 이해가 잘 되지 않았습니다.
하지만 이 의문은 서버와 클라이언트의 실행 시점 차이상태의 연속성을 이해하며 해결되었습니다.

SSR의 목적은 "빠른 초기 화면"과 "SEO"입니다.
서버는 요청 시점의 데이터를 기반으로 완성된 HTML을 보내주지만, 브라우저에 도착한 자바스크립트는 서버가 어떤 데이터를 사용했는지 알 수 없습니다.

만약 초기 데이터를 넘겨주지 않으면 다음과 같은 문제가 발생하게 됩니다.

  • 화면 깜빡임: 완성된 HTML이 화면에 보이다가 JS가 실행되는 순간 데이터가 없는 "초기 상태"로 덮어씌워집니다.
  • 하이드레이션 오류: 클라이언트가 그리려는 "빈 상태"와 서버가 보내준 "데이터가 있는 HTML"이 일치하지 않아 동기화에 실패합니다.

결국 window.__INITIAL_DATA__는 서버에서 고생해서 가져온 데이터를 클라이언트에 고스란히 인계하는 역할입니다.
이 덕분에 클라이언트는 추가 API 호출 없이도 서버와 동일한 상태을 가지고 렌더링을 이어갈 수 있습니다.


서버 인스턴스 상태 격리와 요청간의 독립성

발제 당시 준일 코치님이 "동시성 문제"에 대해 언급하셨었는데.. 과제를 직접 구현해보기 전까지는 잘 와닿지 않았습니다.
하지만 SSR을 직접 구현하며 서버는 수많은 요청이 상태를 공유하는 위험한(?) 환경이라는 것을 느꼈습니다.

서버는 공용 공간이다.

브라우저 환경에서는 한 명의 사용자가 하나의 인스턴스를 독점하지만, Node.js 서버는 싱글 스레드 이벤트 루프 모델 위에서 수많은 사용자의 요청을 동시에 처리합니다.
만약 서버에서 스토어를 싱글톤이나 전역 변수 형태로 관리하게 된다면, 모든 요청이 해당 상태를 공유하게 됩니다.

특히 비동기 처리가 일어날 때 문제가 심각해집니다.
A 사용자의 요청을 처리하다가 데이터를 가져오기 위해 await 하는 사이, 이벤트 루프는 B 사용자의 요청을 처리하기 시작합니다.
만약 이때 같은 스토어 인스턴스를 공유하고 있다면, B의 데이터가 A의 스토어를 덮어쓰게 되고, 결국 A는 B의 데이터를 담은 HTML을 받게 되는 치명적인 사고가 발생합니다..

아키텍처적 격리의 필수성

이 과정에서 깨달은 것은 서버 사이드 렌더링의 핵심 아키텍처는 완벽한 격리에 있다는 것입니다.

  • 상태 격리 전략: 각 요청마다 독립적으로 데이터를 관리해야 합니다.
  • 클린업: 렌더링 사이클이 끝나면 사용된 메모리와 상태를 즉시 해제하거나 초기화해야 합니다.

자유롭게 회고하기

과제 시작 전: 익숙함이 만든 "근거 없는 자신감"

과제를 시작하기 전에는 CSR, SSR, SSG의 개념적 정의와 차이, 그리고 어떤 상황에서 어떤 렌더링 전략을 선택해야 하는지에 대해서는 충분히 이해하고 있다고 생각했습니다.
실제로 Next.js를 사용하며 페이지 단위 SSR/SSG는 물론, 하이브리드 렌더링과 컴포넌트 단위에서의 CSR 분리 등 다양한 렌더링 전략을 적용해왔기 때문에, 이론과 사용 경험 모두 익숙한 영역이라고 판단했습니다.

하지만 이는 프레임워크가 제공하는 추상화 위에서의 이해에 가까웠습니다.
서버와 클라이언트가 어떤 시점에 어떤 역할을 수행하는지, 요청 단위로 상태를 어떻게 격리하는지, 렌더링 결과가 어떤 경로로 전달되고 다시 이어지는지와 같은 렌더링 과정 자체를 직접 구현해본 경험은 없었습니다.

막상 구현을 시작하니, 프레임워크 없이 서버와 클라이언트가 공유할 코드와 분리할 코드의 경계를 직접 설정하고, 각 요청마다 상태를 격리하면서도 렌더링 파이프라인의 일관성을 유지해야 하는 문제들이 생각보다 훨씬 복잡하게 다가왔습니다.

그래서 이번에도 노션 발제 자료와 발제 영상, 그리고 준일 코치님의 블로그를 참고하며 전체 흐름을 다시 정리해 나갔습니다.
특히 준일 코치님의 "프레임워크 없이 만드는 SSR" 글에서 다뤄진 특정 환경에 종속적/독립적인 코드의 분리에 대한 설명이 이번 과제를 이해하는 데 큰 도움이 되었습니다.

프레임워크 없이 SSR을 구현해보니, 그동안 알고 있던 지식이 "무엇을 선택해야 하는가"에 머물러 있었을 뿐, "그 선택이 어떤 메커니즘으로 동작하는가"까지는 깊이 이해하지 못하고 있었다는 점을 체감하게 되었습니다.

이로 인해 과제를 단순한 개념 복습으로 예상했던 초반 판단이 잘못되었음을 깨달았고, 이번 과제는 CSR/SSR/SSG를 "사용하는 입장"이 아닌 "구현하는 입장"에서 다시 바라보는 전환점이 되었습니다.


Vanilla SSR: 전역 변수를 통한 상태 격리와 렌더링 파이프라인

공부한 내용을 바탕으로 바닐라 SSR을 구현하며, 서버 사이드 렌더링 함수가 라우트 매칭, 데이터 프리페칭, HTML 생성, 초기 데이터 주입까지 모든 과정을 유기적으로 처리하도록 설계했습니다.

전역 변수를 통한 데이터 전달

바닐라 환경의 특성상 global.__SSR_DATA__global.router를 활용해 서버 데이터를 페이지 컴포넌트에 전달하는 패턴을 적용했습니다.

global.__SSR_DATA__ = storeData;
global.router = router;

const html = routeInfo.handler();

delete global.__SSR_DATA__;
delete global.router;

페이지 컴포넌트 내에서는 isServer() 체크를 통해 서버와 클라이언트의 데이터 출처를 구분했습니다.

const productState = isServer() && global.__SSR_DATA__ 
  ? global.__SSR_DATA__ 
  : productStore.getState();

상태 격리와 클린업

서버 인스턴스는 요청 간 상태를 공유하기 때문에, 렌더링 사이클이 완전히 끝난 직후 delete 키워드로 전역 변수를 즉시 삭제했습니다.
이는 단순히 버그를 피하는 기법이 아니라, 서버 아키텍처의 격리 원칙을 지키기 위한 필수적인 과정이었습니다.

라우트별 트레이드오프 고려

페이지별로 요구하는 데이터 구조가 달랐기에, 서버에서 주입하는 데이터를 최소화하면서도 클라이언트가 하이드레이션에 필요한 모든 정보를 포함하도록 세밀하게 설계했습니다.

let initialData;

if (routeInfo.path === "/") {
  initialData = {
    products: storeData.products,
    categories: storeData.categories,
    totalCount: storeData.totalCount,
  };
} else if (routeInfo.path === "/product/:id/") {
  initialData = {
    currentProduct: storeData.currentProduct,
    relatedProducts: storeData.relatedProducts,
  };
} else {
  initialData = storeData;
}

const data = initialData 
  ? `<script>window.__INITIAL_DATA__=${JSON.stringify(initialData)};</script>` 
  : null;

React SSR & SSG: 선언적 모델로의 전환과 정적 생성의 복잡성

바닐라 구현의 경험을 React로 옮기며, 라이브러리의 철학에 맞는 또 다른 방식의 격리가 필요함을 체감했습니다.

Context API를 통한 의존성 주입

전역 변수를 사용했던 바닐라와 달리, React에서는 각 요청마다 새로운 Router 인스턴스를 생성하고 RouterContext.Provider를 통해 데이터를 전달했습니다.

productStore.dispatch({ 
  type: PRODUCT_ACTIONS.SETUP, 
  payload: { ...initialProductState, ...storeData } 
});

const PageComponent = routeInfo.handler;
const html = renderToString(
  <RouterContext.Provider value={router}>
    <PageComponent />
  </RouterContext.Provider>,
);

productStore.dispatch({
  type: PRODUCT_ACTIONS.SETUP,
  payload: { ...initialProductState }
});

이는 각 렌더링 사이클이 독립적이어야 하는 React의 선언적 모델에 훨씬 적합한 방식이었습니다.
렌더링 전에 상태를 설정하고, 렌더링 후에는 즉시 초기 상태로 리셋하여 요청 간 격리를 보장했습니다.

createRoot vs hydrateRoot

가장 큰 배움은 클라이언트 엔트리 포인트의 변화였습니다.
빈 컨테이너에서 DOM을 처음부터 빌드하는 createRoot와 달리, hydrateRoot는 서버가 이미 만든 HTML을 인정하고 그 위에 이벤트 리스너만 효율적으로 부착한다는 점을 명확히 이해했습니다.

if (rootElement.hasChildNodes()) {
  hydrateRoot(rootElement, app);
} else {
  createRoot(rootElement).render(app);
}

이 과정에서 서버와 클라이언트의 결과물이 텍스트 하나라도 다르면 경고를 뱉는 React의 엄격함을 보며, 왜 SSOT가 중요한지 다시금 깨달았습니다.
hasChildNodes() 체크를 통해 SSG로 생성된 페이지와 CSR로 접근한 페이지를 구분하여 적절한 렌더링 방식을 선택했습니다.

TypeScript와 타입 안전성

RouteInfoType, PrefetchDataType 등을 명시적으로 정의하여, 바닐라에서는 런타임에나 발견할 수 있었던 에러를 컴파일 타임에 잡아내며 설계의 견고함을 더했습니다.

export type RouteInfoType = ReturnType<InstanceType<typeof Router<FunctionComponent>>["match"]>;
export type PrefetchDataType = Awaited<ReturnType<typeof prefetchData>>;

이러한 타입 정의를 통해 generateHead 함수나 prefetchData 함수의 반환값을 타입 안전하게 사용할 수 있었고, 컴파일 타임에 타입 불일치를 발견할 수 있었습니다.

리뷰 받고 싶은 내용

서버 데이터 전달 방식: global 참조 vs 상태 dispatch 및 초기화

바닐라와 React 환경 각각의 특성에 맞춰 서버 데이터를 컴포넌트에 주입하는 방식을 다르게 설계했습니다.
이 두 접근 방식의 적절성과 아키텍처적 일관성에 대해 피드백을 받고 싶습니다.

환경별 구현 방식

  • Vanilla 방식: 전역 변수(global) 주입 및 삭제
    렌더링 직전 global.__SSR_DATA__에 데이터를 할당하고, 컴포넌트 내부에서 isServer() 분기로 이를 참조합니다.
    렌더링이 완료된 직후 delete를 통해 할당된 데이터를 명시적으로 정리합니다.

  • React 방식: Store dispatch 및 Reset
    싱글톤으로 생성된 Store 인스턴스에 데이터를 dispatch하여 상태를 설정한 뒤 renderToString을 수행합니다.
    이후 다시 초기 상태로 리셋하여 다음 요청에 대비합니다.

각 방식을 선택한 이유

  • Vanilla: 서버/클라이언트 책임 분리에 집중
    서버 렌더링을 단순 HTML 문자열 생성 과정으로 정의했습니다.
    서버에서는 Store 구독이 발생하지 않으므로, Store를 조작하기보다 전역 객체(global)를 통해 데이터를 전달하는 것이 환경 간의 역할을 가장 명확히 분리하는 방식이라 판단했습니다.

  • React: 상태 일관성과 요청 간 격리에 집중
    컴포넌트가 useStore를 통해 상태를 구독하는 구조이므로, 서버에서도 Store에 데이터를 직접 설정해야 일관된 데이터 접근이 가능하다고 판단했습니다.
    Store가 싱글톤으로 공유되기에, renderToString 전후로 setup -> reset 과정을 거쳐 요청 간 상태 격리를 보장하고자 했습니다.

질문 내용

  1. 설계의 일관성 vs 환경별 최적화
    바닐라의 "책임 분리(전역 객체)"와 React의 "상태 일관성(Store 활용)" 중 어떤 가치가 SSR 설계에서 더 우선되어야 할까요?
    바닐라 또한 React처럼 Store를 직접 조작하는 방식으로 일원화하는 것이 더 나은 설계일지 궁금합니다.

  2. 동시 요청 상황에서의 안정성
    현재 Store가 싱글톤으로 관리되고 있어 renderToString 전후로 상태를 setup -> reset 하고 있습니다.
    Node.js의 싱글 스레드 환경에서 비동기 작업이 섞일 때, 다른 요청에 의해 Store 상태가 덮어씌워질 위험은 없을지, 아니면 요청마다 Store 인스턴스를 새로 생성하는 방식으로 변경하는 것이 더 안전할지 조언 부탁드립니다.

  3. 메모리 관리
    전역 객체 삭제와 Store 상태 리셋 중, 잦은 요청이 발생하는 SSR 서버 환경에서 메모리 해제 및 가비지 컬렉션 측면에서 더 유리한 패턴이 무엇인지 궁금합니다.


Universal Router 구현 방식의 차이: 외부 분기 vs 내부 분기

Vanilla와 React에서 Universal Router를 구현하는 방식이 다릅니다.

구현 방식

  • Vanilla: 외부 분기 방식
    UniversalRouter 클래스에서 isServer()로 분기하여 ClientRouterServerRouter를 각각 별도 클래스로 구현하고, 생성 시점에 적절한 인스턴스를 선택합니다.
  • React (lib): 내부 분기 방식
    Router 클래스 하나에서 내부적으로 isServer()를 체크하여 서버/클라이언트 로직을 분기합니다.
    별도의 ClientRouter, ServerRouter 클래스는 없습니다.

각 방식을 선택한 이유

  • Vanilla: 환경별 책임 분리와 런타임 안전성에 집중
    ClientRouterServerRouter를 별도 클래스로 구현하여 서버 전용 메서드(match)와 클라이언트 전용 메서드(push, start)를 명시적으로 분리했습니다.
    바닐라 환경에서는 TypeScript가 없어 타입 안전성이 보장되지 않으므로, 서버 환경에서 window 객체에 접근하려는 시도를 런타임에 방지하고 각 환경의 책임을 명확히 구분할 수 있다고 판단했습니다.

  • React (lib): 코드 중복 최소화와 라이브러리 범용성에 집중
    Router 클래스 하나에서 모든 메서드를 제공하고 내부에서 isServer()로 분기하여 클라이언트 전용 로직을 조건부로 실행합니다.
    라이브러리로 제공되는 범용성을 고려하여 단일 인터페이스를 유지하면서도 코드 중복을 최소화하고자 했습니다.

질문 내용

  1. 클래스 분리 vs 내부 분기의 트레이드오프
    Vanilla 방식은 Strategy 패턴처럼 각 환경별 구현을 명시적으로 분리하여 타입 안전성과 테스트 용이성을 높이지만, 코드 중복과 유지보수 비용이 증가할 수 있습니다.
    React 방식은 단일 클래스 내부에서 분기하여 코드 중복을 줄이지만, 서버/클라이언트 로직이 섞여 복잡도가 증가할 수 있습니다.
    실제 프로덕션 환경에서 두 방식의 성능 오버헤드와 메모리 사용량 차이는 측정 가능한 수준일까요?

  2. 타입 안전성과 런타임 안정성
    Vanilla의 명시적 클래스 분리는 서버 전용 메서드(match)와 클라이언트 전용 메서드(push, start)를 타입 레벨에서 구분할 수 있지만, React 방식은 런타임에만 체크합니다.
    TypeScript를 사용하는 환경에서 두 방식의 타입 추론 능력과 컴파일 타임 에러 감지율 차이는 어느 정도일까요? 또한 서버 환경에서 클라이언트 전용 메서드가 호출되거나 그 반대의 경우를 방지하는 메커니즘의 필요성은 어느 정도일까요?

- express 미들웨어 기반 서버 구현
- 개발/프로덕션 환경 분기 처리
- HTML 템플릿 치환
- isServer() 유틸리티 함수 추가
- 서버 사이드에서 사용할 InMemoryStorage 클래스 구현
- createStorage가 서버에서 InMemoryStorage 사용하도록 수정
- ServerRouter: 서버 환경에서 동작하는 라우터 구현
- ClientRouter: 클라이언트 환경에서 동작하는 라우터 구현
- UniversalRouter: 환경에 따라 적절한 라우터를 선택하는 래퍼
- router.js에서 UniversalRouter 사용하도록 수정
- productApiServer: 서버 사이드 전용 API 함수 구현 (productUtils 사용)
- handlers.js에서 공통 유틸리티 사용하도록 수정
- 중복 코드 제거
- 서버에서 동작하는 Router를 사용한 렌더링 (productApiServer 사용)
- 상품 목록 및 상품 상세 데이터 프리페칭
- 서버 상태관리 초기화 (global.__SSR_DATA__ 사용)
- window.__INITIAL_DATA__ 스크립트 주입
- withLifecycle에서 isServer() 사용하도록 수정
- window.__INITIAL_DATA__를 통한 클라이언트 상태 복원
- 서버-클라이언트 데이터 일치 보장
- SSR 데이터가 있을 경우 불필요한 API 호출 방지
- 페이지 컴포넌트에서 isServer()로 서버 데이터 사용
- 동적 라우트 SSG (상품 상세 페이지들)
- 빌드 타임 페이지 생성
- 파일 시스템 기반 배포
- main-server.js의 render 함수 사용
- 클라이언트 사이드 네비게이션 시 document.title 자동 업데이트 기능 추가
- 서버와 클라이언트에서 title 생성 로직 통일 (generateTitle 공통 함수)
- render 함수에서 title 업데이트 처리하여 중복 호출 제거
- includeImages 파라미터를 제거하고 항상 images를 포함하도록 변경
- productApiServer와 handlers 모두 동일한 방식으로 사용
- lib Router에 SSR 지원 추가 (matchRoute, 서버/클라이언트 분기)
- router.ts 삭제
- router/index.ts에서 RouterContext export로 변경
- RouterContext 추가 (createContext 사용)
- useRouterContext hook 추가 (에러 처리 포함)
- router hooks들이 Context를 통해 router 사용하도록 변경
- 전역 router 인스턴스 제거
- 모든 컴포넌트에서 useRouterContext() 사용
- productUseCase 함수들이 router를 인자로 받도록 변경
- main.tsx와 main-server.tsx에서 Router 인스턴스 생성 및 Context 제공
- react/utils/isServer.ts 추가
- 모든 typeof window === "undefined" 체크를 isServer()로 변경
- isClient 제거하고 isServer()만 사용
- URL 쿼리 변경 시 loadProducts 자동 호출
- 검색 및 필터링 기능 동작 보장
- InMemoryStorage 클래스 추가 (서버용 메모리 스토리지)
- isServer() 유틸리티 사용
- createStorage에서 서버/클라이언트 자동 분기
- utils/updateTitle.ts 추가 (generateTitle, updateTitle)
- App.tsx에서 라우터 변경 감지하여 타이틀 업데이트
- main-server.tsx에서 generateTitle 사용
- GetProductsParams 인터페이스 추가
- 모든 함수에 타입 지정 (any 제거)
- main-server.tsx에서 productApiServer import로 변경
- productUtils.ts 추가 (공통 유틸리티 함수)
- getSnapshot을 함수로 분리하여 SSR 환경에서 안정적으로 동작하도록 수정
- window.__INITIAL_DATA__ 확인하여 불필요한 API 호출 방지
- Express SSR 서버 구현 (vanilla 참고)
- 개발/프로덕션 환경 분기 처리
- HTML 템플릿 치환 (<!--app-head-->, <!--app-html-->, <!--app-data-->)
- SSG 빌드 스크립트 업데이트
- routeInfo에서 path, params 구조 분해 할당으로 변경
- React와 동일한 패턴으로 통일
- window.__INITIAL_DATA__ 체크 대신 store 상태 기반으로 변경
- useProductFilter에서 첫 마운트 건너뛰고 쿼리 변경 시에만 데이터 로드
- useLoadProductDetail에서 store 상태 체크로 SSR 데이터 재사용
- main.tsx에서 currentProduct 복원 시 status 상태 설정
- searchProducts, setCategory, setSort, setLimit 함수에서 router.query 설정 후 loadProducts를 직접 호출하도록 수정
- URL은 업데이트되지만 실제 필터링이 동작하지 않던 문제 해결
- App 컴포넌트에서 currentProduct를 useEffect 의존성 배열에 추가
- 상품 데이터 로드 후 타이틀이 '상품명 - 쇼핑몰'로 업데이트되도록 수정
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant